跳转到主要内容

Products

A product is an article you buy or sell as a finished item: a packaged good, a ready-made component you buy in, an article from a supplier's catalogue.

You store it once, under your own id. From then on any recipe can reference it — and referencing a product never changes it.

Why this matters

An ordinary ingredient inside a recipe is defined by that recipe. If two recipes use the id onions and one of them sends different data, it changes onions for both. That is fine when you are describing a one-off ingredient, and a trap when you are managing a catalogue.

A product is the way out. Only the product endpoints may write it, so a recipe upload can never overwrite it — and every recipe that references it picks up your improvements automatically.

Product vs Recipe

Products are articles: things you buy or sell as a unit. Their composition usually comes from the ingredient declaration on the packaging.

Recipes are meals prepared in a kitchen, where you know the ingredients and how much of each goes in.

Coordination Required

We recommend using the Product resources in tight coordination with the Eaternity team, as the calculations require additional supervision.

Storing a product

PUT /products/{id}, with your own id in the URL. The response already contains the finished calculation — there is no second request to make.

PUT /products/carrot-puree-500g
{
"id": "carrot-puree-500g",
"gtin": "00123456789023",
"names": [{"language": "de", "value": "Karottenpüree"}],
"amount": 500,
"unit": "gram",
"producer": "ACME Foods",
"ingredients-declaration": "Karotten, Wasser, Salz",
"nutrient-values": {
"energy-kcal": 35,
"fat-gram": 0.2,
"protein-gram": 0.9,
"carbohydrates-gram": 7.5
}
}

The ingredients-declaration is what tells us what the product is really made of, so include it whenever the packaging has one. Without it, the product is identified from its name alone.

To upload a catalogue, use POST /products/batch. Each product is handled on its own and gets its own entry in the response, so one product that cannot be calculated does not hold up the rest. Match the entries to your request by product-id, or by the request-id you sent along.

Once uploaded, keep your local copy in step with GET /products?updated-since — see Keeping your data up to date.

Using a product in a recipe

Reference it by id, with type: "products":

A recipe referencing the stored product
{
"id": "carrot-soup",
"names": [{"language": "de", "value": "Karottensuppe"}],
"ingredients": [
{
"id": "carrot-puree-500g",
"type": "products",
"amount": 300,
"unit": "gram"
},
{
"id": "onions",
"type": "conceptual-ingredients",
"names": [{"language": "de", "value": "Zwiebeln"}],
"amount": 50,
"unit": "gram"
}
]
}

You send no name for the product line — just the id and how much of it goes in. The recipe is calculated from the product's real composition, and the product itself is not touched.

A products line resolves only a product. If no product with that id exists, the recipe comes back with statuscode 612 — and an id that exists only as an ordinary ingredient (a conceptual-ingredients line some recipe defined on the fly) is rejected the same way. Store it as a product first if you want to reference it.

Define once, reference everywhere

Improve the product, and every recipe referencing it improves with it. Describe the same article inline in fifty recipes, and you have fifty things to fix.

Products are write-protected

Once an id belongs to a product, only the product endpoints may change it.

A recipe or supply that tries to define an ingredient under that id — a conceptual-ingredients line — is rejected with statuscode 613. Referencing it with type: "products" is always fine: a reference writes nothing, so there is nothing to protect against.

The reverse case is guarded too. Storing a product under an id that already exists as an ordinary ingredient converts that ingredient into a write-protected product — and any recipe still sending it as a conceptual-ingredients line will start being rejected with 613. Because that is a one-way change, it is refused unless you confirm it:

PUT /products/{id}?convert-ingredient-to-product=true

Keeping your data up to date

The values we calculate are not frozen. They improve over time — an ingredient gets matched more precisely, background data is refreshed — so a product you uploaded months ago may have a better CO₂ value today.

GET /products returns the ids of your products. Add updated-since with the date of your last synchronisation, and you get back only the ones worth fetching again:

GET /products?updated-since=2026-07-01
{
"products": ["carrot-puree-500g", "tomato-sauce-400g"]
}

That is: the products you changed, the ones never calculated, and the ones whose values may have moved because something they rely on improved. Fetch each with GET /products/{id}.

The catalogue loop

Uploading in bulk and syncing changes are two halves of the same workflow, and they compose cleanly — a product uploaded through POST /products/batch behaves in the feed exactly like one uploaded with a single PUT:

  1. Upload your whole catalogue once with POST /products/batch.
  2. Store the date of that upload.
  3. Later, GET /products?updated-since=<that date> returns only what has moved since — the products you re-uploaded (in bulk or one at a time) and the ones whose values improved on our side.
  4. Fetch just those with GET /products/{id}, and store the new sync date.

So you upload the catalogue in one call and, from then on, only ever re-fetch the handful that actually changed.

Reading and deleting

GET /products/{id} returns the product and its calculated values.

The same call also finds an ordinary ingredient that one of your recipes created under that id. The type field says which you got — and it is exactly the type you would use to reference it:

typeMeaning
productsWrite-protected. Only the product endpoints may change it.
conceptual-ingredientsAn ordinary ingredient. Any recipe or supply may overwrite it.

DELETE /products/{id} removes either kind. Afterwards the id no longer appears in GET /products.

Product Properties

Request Properties

PropertyTypeRequiredDescription
idStringYesYour unique product identifier
namesArrayYesProduct name as array of language objects: [{"language": "de", "value": "Karottenpüree"}]
ingredients-declarationStringBest practiceList of ingredients as declared on the packaging. Parsed to determine what the product is made of.
nutrient-valuesObjectBest practiceNutrient values per 100g as declared on the packaging (see Reference)
gtinStringBest practiceGlobal Trade Item Number (www.gtin.info)
amountFloatBest practiceAmount of the product in the specified unit. Default: 100
unitStringNoUnit: kg, gram, or liter. Default: gram
producerStringBest practiceThe producer or brand of the product
dateStringNoProduction date for seasonal calculations. Format: YYYY-MM-DD
originStringBest practiceProduction location: postal address or country
transportStringBest practiceMeans of transport: air or ground
productionStringBest practiceProduction method: standard, greenhouse, organic, fair-trade, farm, wild-caught, sustainable-fish
processingStringNoProcessing level: raw, unboned, boned, skinned, beheaded, fillet, cut, boiled, peeled
conservationStringBest practiceStorage method: fresh, frozen, dried, conserved, canned, boiled-down
packagingStringNoPackaging type: none, plastic, paper, pet, tin, alu, glas, cardboard, tetra

Response Properties

The product is returned under product, next to a statuscode that says whether it could be calculated.

PropertyTypeDescription
statuscodeInteger200 when the product was calculated. See Status Codes.
messageStringExplains the statuscode when it is not 200
product-idStringThe id of the product this entry belongs to
productObjectThe product and its calculated values (below)
PropertyTypeDescription
idStringProduct identifier
typeStringproducts (write-protected) or conceptual-ingredients (any recipe may overwrite it)
namesArrayProduct names as array of language objects
co2-valueIntegerCO₂ emissions in grams CO₂e for the specified amount (see the note below)
ratingStringCO₂ rating (A-E). A is best, E is worst.
eaternity-awardBooleantrue if the product is climate friendly
co2-value-improvement-percentageFloatComparison of this product's CO₂ footprint per food unit to the average
co2-value-reduction-valueFloatGrams of CO₂ saved compared to an average product providing the same nutritional value
food-unitFloatNutritional value of the product for the specified amount
amountFloatProduct amount, echoed back from your request
unitStringUnit of measurement
gtinStringGlobal Trade Item Number
dateStringProduction date
info-textStringNotes on the calculation (e.g. "No cooking date was provided.")
ingredientsArrayThe ingredients the product is made of, with their individual ratings and CO₂ values
ingredients-declarationStringThe original ingredient declaration text (when ingredients-declaration=true is set)
nutrient-values-estimated-per-specified-amountObjectBackend-estimated nutrient values scaled to the specified amount (when nutrient-values=true is set)
indicatorsObjectEnvironmental indicators (when indicators=true is set)
A product's footprint is for the amount you send

co2-value and food-unit are calculated for the amount (and unit) on your request — ten times the amount is ten times the footprint. 500 gram and 0.5 kilogram give the same result. If you send no amount, the footprint is for one kilogram.

When you instead reference the product from a recipe, it is the amount on that recipe line that scales it — the product's own amount does not carry over.

Response Example

{
"statuscode": 200,
"message": "",
"product-id": "carrot-puree-500g",
"product": {
"id": "carrot-puree-500g",
"type": "products",
"names": [{"language": "de", "value": "Karottenpüree"}],
"co2-value": 478,
"rating": "A",
"eaternity-award": true,
"co2-value-improvement-percentage": -41.0,
"co2-value-reduction-value": -140.0,
"food-unit": 0.3,
"amount": 500,
"unit": "gram",
"gtin": "00123456789023",
"info-text": "No cooking date was provided.",
"ingredients-declaration": "Karotten, Wasser, Salz",
"ingredients": [
{
"names": [{"language": "de", "value": "Karotten"}],
"amount": 100.0,
"unit": "gram",
"rating": "A",
"co2-value": 32,
"foodUnit": 0.11
}
]
}
}

Status Codes

Next to the usual status codes, products use:

CodeMeaningWhat to do
612The product you referenced does not existStore the product first, then send the recipe
613A write a product does not accept — either a recipe tried to define an ingredient under a product's id, or a product write would convert an existing ingredientReference the product with type: "products" instead of defining it. To convert an ingredient on purpose, add convert-ingredient-to-product=true.

Endpoints