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.
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.
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.
{
"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":
{
"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.
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:
- Upload your whole catalogue once with
POST /products/batch. - Store the date of that upload.
- 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. - 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:
type | Meaning |
|---|---|
products | Write-protected. Only the product endpoints may change it. |
conceptual-ingredients | An 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
| Property | Type | Required | Description |
|---|---|---|---|
id | String | Yes | Your unique product identifier |
names | Array | Yes | Product name as array of language objects: [{"language": "de", "value": "Karottenpüree"}] |
ingredients-declaration | String | Best practice | List of ingredients as declared on the packaging. Parsed to determine what the product is made of. |
nutrient-values | Object | Best practice | Nutrient values per 100g as declared on the packaging (see Reference) |
gtin | String | Best practice | Global Trade Item Number (www.gtin.info) |
amount | Float | Best practice | Amount of the product in the specified unit. Default: 100 |
unit | String | No | Unit: kg, gram, or liter. Default: gram |
producer | String | Best practice | The producer or brand of the product |
date | String | No | Production date for seasonal calculations. Format: YYYY-MM-DD |
origin | String | Best practice | Production location: postal address or country |
transport | String | Best practice | Means of transport: air or ground |
production | String | Best practice | Production method: standard, greenhouse, organic, fair-trade, farm, wild-caught, sustainable-fish |
processing | String | No | Processing level: raw, unboned, boned, skinned, beheaded, fillet, cut, boiled, peeled |
conservation | String | Best practice | Storage method: fresh, frozen, dried, conserved, canned, boiled-down |
packaging | String | No | Packaging 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.
| Property | Type | Description |
|---|---|---|
statuscode | Integer | 200 when the product was calculated. See Status Codes. |
message | String | Explains the statuscode when it is not 200 |
product-id | String | The id of the product this entry belongs to |
product | Object | The product and its calculated values (below) |
| Property | Type | Description |
|---|---|---|
id | String | Product identifier |
type | String | products (write-protected) or conceptual-ingredients (any recipe may overwrite it) |
names | Array | Product names as array of language objects |
co2-value | Integer | CO₂ emissions in grams CO₂e for the specified amount (see the note below) |
rating | String | CO₂ rating (A-E). A is best, E is worst. |
eaternity-award | Boolean | true if the product is climate friendly |
co2-value-improvement-percentage | Float | Comparison of this product's CO₂ footprint per food unit to the average |
co2-value-reduction-value | Float | Grams of CO₂ saved compared to an average product providing the same nutritional value |
food-unit | Float | Nutritional value of the product for the specified amount |
amount | Float | Product amount, echoed back from your request |
unit | String | Unit of measurement |
gtin | String | Global Trade Item Number |
date | String | Production date |
info-text | String | Notes on the calculation (e.g. "No cooking date was provided.") |
ingredients | Array | The ingredients the product is made of, with their individual ratings and CO₂ values |
ingredients-declaration | String | The original ingredient declaration text (when ingredients-declaration=true is set) |
nutrient-values-estimated-per-specified-amount | Object | Backend-estimated nutrient values scaled to the specified amount (when nutrient-values=true is set) |
indicators | Object | Environmental indicators (when indicators=true is set) |
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:
| Code | Meaning | What to do |
|---|---|---|
| 612 | The product you referenced does not exist | Store the product first, then send the recipe |
| 613 | A 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 ingredient | Reference the product with type: "products" instead of defining it. To convert an ingredient on purpose, add convert-ingredient-to-product=true. |
Endpoints
📄️ List products
Returns the ids of all products stored for your account.
📄️ Create or update a batch of products
Create or update many products in a single request - the recommended way to upload a product
📄️ Create or update a product
Create or update a single product. The response already contains the finished calculation, so
📄️ Get a product
Retrieve a product together with its calculated values.
📄️ Delete a product
Delete a product. Like the read, this also resolves an ordinary ingredient stored under this