Migration Guide

This guide covers the breaking changes between API versions and how to update your integration. Pick the section that matches your upgrade path:

Migrating from v2 to v3

v3 is the recommended version for all new integrations. The key changes are: proper JSON request bodies, PATCH for updates, and granular nested collection management. Authentication stays the same.

Authentication

v3 uses the same Bearer token format as v1/v2. Your API token and header stay the same:

curl -H "Authorization: Bearer YOUR_TOKEN" \
  https://api.eclarion.com/v3/recipes

Reference endpoint changes

Reference data endpoints remain public (no authentication required), same as v1/v2. Two endpoints have been renamed and one is new:

v2v3Notes
/v2/microbiologyprecisions/v3/microbiology_precisionsRenamed (underscores)
/v2/microbiologymetricunits/v3/microbiology_metric_unitsRenamed (underscores)
/v3/measurement_unitsNew (replaces measurement_unit_id integers in line items)

Request body format

v2 sends recipe data as a URL-encoded JSON string. v3 uses proper JSON:

Before (v2):

curl -X POST \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d 'recipe={"productname":{"en":"Fruit Yoghurt"}}' \
  https://api.eclarion.com/v2/recipes

After (v3):

curl -X POST \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"recipe": {"productname": {"en": "Fruit Yoghurt"}}}' \
  https://api.eclarion.com/v3/recipes

PATCH replaces PUT

Recipe updates use PATCH instead of PUT. Only send the fields you want to change:

Before (v2):

curl -X PUT \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d 'recipe={"productname":{"en":"Updated Name"},"ingredientdeclaration":{"en":"..."}}' \
  https://api.eclarion.com/v2/recipes/123

After (v3):

curl -X PATCH \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"recipe": {"productname": {"en": "Updated Name"}}}' \
  https://api.eclarion.com/v3/recipes/123

Response format

v3 responses are wrapped in a root key and paginated results include a meta object:

{
  "recipes": [...],
  "meta": {
    "page": 1,
    "items": 50,
    "count": 234,
    "pages": 5
  }
}

Use the page and items query parameters to control pagination. See the List recipes endpoint for details.

Renamed fields

v2 fieldv3 fieldNotes
nutrientsnutritional_valuesNested collection renamed
measurement_unit_idmeasurement_unitNow a reference string (e.g. kilogram) instead of integer ID

Type changes

Several fields have changed type for consistency:

Fieldv2 typev3 type
claimsStringObject
metal_detectionStringObject
countries_of_originStringArray of strings

New: recipe steps

v3 introduces steps — named production phases with their own ingredients. This is a completely new concept:

{
  "recipe": {
    "steps": [
      {
        "position": 1,
        "name": { "en": "Mixing" },
        "instructions": { "en": "Combine all dry ingredients." },
        "line_items": [
          { "ingredient_id": 150, "weight": 500, "measurement_unit": "gram" },
          { "ingredient_id": 151, "weight": 200 }
        ]
      },
      {
        "position": 2,
        "name": { "en": "Baking" },
        "instructions": { "en": "Bake at 180°C for 25 minutes." }
      }
    ]
  }
}

Steps are optional. You can still use recipe-level line_items for simple recipes without production phases. See Steps vs. line items in the Recipes guide.

New: granular nested collection updates

In v2, updating nested collections replaces the entire array. In v3, you can update, add, and remove individual records in a single request.

Reference-based collections (nutritional_values, allergen_values, microbiological_values, physiochemical_values, organoleptic_values, preparation_instruction_values) — identified by reference:

ActionHow
Update existingUse the reference: {"reference": "protein", "value": 7.5}
Add newSame syntax: {"reference": "fiber", "value": 3.0}
Removereference + remove: {"reference": "fat", "remove": true}

ID-based collections (line_items, packages, steps) — identified by id:

ActionHow
Update existingInclude the id: {"id": 456, "weight": 200}
Add newOmit the id: {"ingredient_id": 123, "weight": 100}
Removeid + remove: {"id": 456, "remove": true}
{
  "recipe": {
    "nutritional_values": [
      { "reference": "protein", "value": 7.5 },
      { "reference": "fiber", "value": 2.1 },
      { "reference": "fat", "remove": true }
    ]
  }
}

New fields in v3

FieldTypeNotes
codeStringRecipe code
versionStringRecipe version
created_atISO 8601Creation timestamp
updated_atISO 8601Last update timestamp

Summary of v2 to v3 changes

  1. Authentication: Bearer TOKEN (same as v1/v2)
  2. Request body: URL-encoded JSON string → proper JSON with Content-Type: application/json
  3. Updates: PUT (full replacement) → PATCH (partial updates)
  4. nutrientsnutritional_values
  5. Type changes: nutrients_with_loss, metal_detection (boolean), claims (object), countries_of_origin (array)
  6. Reference endpoints: two renamed with underscores, new /v3/measurement_units
  7. Responses wrapped in root keys with meta pagination object
  8. New: recipe steps with nested line items
  9. New: granular nested collection updates (add/update/remove in one request)
  10. measurement_unit_idmeasurement_unit (reference string, e.g. kilogram)
  11. New: /v3/measurement_units reference endpoint
  12. New: code, version, created_at, updated_at fields

Migrating from v1 to v2

The biggest change from v1 to v2 is consistent multilingual field handling. v2 also adds several new recipe fields.

Multilingual fields: always an object

In v1, multilingual fields return a plain string for single-language accounts and a translations object for multi-language accounts. This means your code needs to handle both types:

v1 (single-language account):

{
  "productname": "Chocolate Cake"
}

v1 (multi-language account):

{
  "productname": {
    "en": "Chocolate Cake",
    "nl": "Chocoladetaart"
  }
}

In v2, multilingual fields always return a translations object, regardless of the number of active languages:

v2 (always):

{
  "productname": {
    "en": "Chocolate Cake"
  }
}

Action required: If your v1 code checks whether the field is a plain string or a translations object, you can simplify it. In v2, always expect an object.

Date fields became multilingual

Three date-related fields changed from plain strings to multilingual objects:

Fieldv1v2
best_before_date"See lid"{"en": "See lid"}
use_by_date"See lid"{"en": "See lid"}
date_of_freezing"N/A"{"en": "N/A"}

Update your code to read and write these fields as translation objects.

Removed fields

Three boolean fields were removed and consolidated into the claims field:

v1 fieldv2 replacement
estimated_signUse claims
free_of_gmoUse claims
irradiatedUse claims

New fields in v2

FieldTypeNotes
declarationBooleanDeclaration enabled flag
microbiology_informationMultilingualFree-text microbiology info
custom_fieldsJSON objectFree-form key-value data
data_block_*_id (12 fields)IntegerData block references for nutrition, packaging, physiochemical, microbiological, statements, shelf life, organoleptic, preparation, declaration, allergen, notes, product description

These fields are additive — your existing code will continue to work without changes. Include them when you need the functionality.

Summary of v1 to v2 changes

  1. Multilingual fields always return a translations object (no more conditional string/object)
  2. best_before_date, use_by_date, date_of_freezing changed from string to multilingual
  3. estimated_sign, free_of_gmo, irradiated removed (use claims)
  4. New fields: declaration, microbiology_information, custom_fields, 12x data_block_*_id

Version comparison

v1 (legacy)v2 (stable)v3 (recommended)
StatusLegacyStableRecommended
Auth headerBearer TOKENBearer TOKENBearer TOKEN
Request formatURL-encoded JSONURL-encoded JSONJSON body
Update methodPUTPUTPATCH
MultilingualConditionalAlways objectAlways object
Pagination metaNoNoYes
Recipe stepsNoNoYes
Nested updatesFull replacementFull replacementGranular (add/update/remove)
Public reference endpointsNoNoYes

Next steps