The Foodlex API
A recipe goes in as JSON, the calculated values come back as JSON: nutrition values, allergens and the finished ingredient list. Built for your ERP, your shop or a tool of your own — wherever the numbers are needed without anyone retyping them into the app.
Three endpoints
All under https://app.foodlex.app/api/v1/, all POST.
| Endpoint | Response |
|---|---|
/v1/nutrition |
Nutrition values per 100 g, per portion and raw — unrounded so you can carry on calculating, plus the rounded label figures |
/v1/allergens |
The fourteen allergen groups of Annex II to Regulation (EU) No 1169/2011, with the position in the text |
/v1/ingredient-list |
The ingredient list in descending order of weight, with allergen emphasis and quantitative declaration (QUID) |
What it does not do
The API is stateless: it creates no recipe, reads none and changes nothing in your data. Every call stands on its own — what goes in is calculated, and nothing is left behind.
Authentication
One key, one header:
Authorization: Bearer flx_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
The key is flx_live_ plus 32 characters. It is shown
exactly once, at creation — only its SHA-256 is stored and it
cannot be recovered from the database. If you lose it, revoke it and get a new one.
One complete call
curl -sS https://app.foodlex.app/api/v1/nutrition \
-H "Authorization: Bearer $FOODLEX_KEY" \
-H 'Content-Type: application/json' \
-d '{
"market": "DE",
"language": "en",
"preparationLossPercent": 12,
"portions": 8,
"ingredients": [
{ "name": "Wheat flour, type 405", "amountGrams": 500,
"nutrients": { "energyKj": 1443, "energyKcal": 341, "fat": 1.1,
"fatSaturated": 0.2, "carbs": 71.8, "sugar": 1.2,
"protein": 10.6, "salt": 0.01 } },
{ "name": "Butter", "amountGrams": 250,
"nutrients": { "energyKj": 3059, "energyKcal": 741, "fat": 82.0,
"fatSaturated": 52.0, "carbs": 0.6, "sugar": 0.6,
"protein": 0.7, "salt": 0.02 } }
]
}'The response, shortened:
{
"apiVersion": "1",
"market": "DE",
"language": "en",
"weights": { "rawGrams": 750, "finalGrams": 660, "portionGrams": 82.5 },
"per100g": { "energyKj": 2251.8939393939395, "energyKcal": 539.0151515151515, … },
"perPortion":{ "energyKj": 1857.8125, … },
"labelPer100g": { "energyKj": "2,252 kJ", "energyKcal": "539 kcal",
"fat": "32 g", "fatSaturated": "20 g", "carbs": "55 g",
"sugar": "1.1 g", "protein": "8.3 g", "salt": "0.02 g" },
"warnings": []
}per100g is unrounded, with every digit. That is
deliberate: if you carry on calculating — to a different serving size, into
a table of your own — you should not start from a rounded intermediate
value. Rounding happens once, at the end, and that is what
labelPer100g holds.
"32 g" on an ordinary space finds nothing. And
labelPer100g exists only for DE, FR and
GB, the three under EU law: Australia rounds to three significant
figures, the US in steps relative to a RACC serving size. There the field is
absent and labelNote says why, in your language — a wrongly
rounded figure looks right. The values in per100g are correct for
every market.
Markets and languages
The request carries both, and both cover everything the app covers:
market |
DE · FR · AU · US · GB — decides the rule set. Default DE |
|---|---|
language |
de · en · fr — affects messages, designations and the number format. Default de |
The allergen identifiers stay English even in a German response: they are keys, not display text. The readable name sits beside them, per market and language.
Limits and quota
| Body | 1 MB |
|---|---|
| Ingredients per request | 200 |
Text for /v1/allergens | 20,000 characters |
| Calls per month | Free 0 · Starter 0 · Pro 2,000 · Team 20,000 |
The count is per workspace, not per key: the quota belongs to the plan, and two keys of the same business should not buy each other extra room.
Errors
One shape, on every endpoint, at every status:
{ "apiVersion": "1",
"error": { "code": "bad_request",
"message": "The request does not match the schema: …",
"field": "ingredients[2].amountGrams" } }| Status | code | When |
|---|---|---|
| 400 | bad_request | The body is not JSON or does not match the schema |
| 401 | unauthorized | No key, unknown, revoked or expired |
| 403 | forbidden | This workspace's plan excludes the API |
| 405 | method_not_allowed | Every endpoint is POST |
| 413 | payload_too_large | Body larger than 1 MB |
| 429 | quota_exceeded | Monthly quota used up |
| 500 | internal_error | Unexpected |
| 503 | unavailable | The key check cannot be reached |
Versioning
The version sits in the path, not in a header — so it shows up in every error message, every log and every line of example code.
| Promised | A field documented in v1 does not disappear and does not change its meaning |
|---|---|
| Promised | An unknown field in the request is ignored, not rejected |
| Allowed | New fields in the response — a client must skip over fields it does not know |
| Not promised | The order of keys in the JSON. Nor the numbers themselves: if a rounding rule or an allergen pattern is corrected, the result changes — that is a bug fix, not a broken contract |
If a field has to change its meaning, /api/v2/… appears beside it.
Getting a key
The API is included in the Pro plan and costs nothing on top. Write to me with what you want to use it for, and I will create the key and send it by a route that does not leave it sitting in a mailbox.
The responsibility stays yours: Foodlex does the arithmetic and the formatting, and it says where each figure comes from. The professional and legal check before printing stays with whoever places the label on the market.