Free plan

Food Database API

A single source for 6.1 million foods. Fetch a complete product record by ID — name, brand, ingredients, allergens, Nutri-Score, NOVA group, images, and more.

200 OK
GET /v1/foods/{id}
curl -H "X-API-Key: food_•••" \
  "https://foodbase.dev/v1/foods/62ea12b8-b260-c288-5c38-3a4906f5d6dd"

{
  "id": "62ea12b8-b260-c288-5c38-3a4906f5d6dd",
  "name_default": "Coca-Cola",
  "brand": "Coca-Cola",
  "category": "Beverages",
  "ingredients_default": "Carbonated water, sugar, ...",
  "allergens": [],
  "nutriscore": "e",
  "nova_group": 4,
  "image_url": "https://images..."
}

Prefer to click around? Try the product explorer or the photo playground.

What you get back

Identity

id, barcode, source, name, brand, category.

Ingredients

Full ingredient text plus structured allergens and traces.

Scores

Nutri-Score, NOVA group, and Eco-Score where available.

Media & locale

Product image URL and multilingual names.

One schema

Every product normalized into one consistent shape.

Included on the free plan — 100 requests/day, no credit card required. See plans →

Code samples

The same request in curl, JavaScript and Python. Replace YOUR_API_KEY with a key from your dashboard.


              curl "https://foodbase.dev/v1/foods/62ea12b8-b260-c288-5c38-3a4906f5d6dd?lang=fr" \
  -H "X-API-Key: YOUR_API_KEY"
            

How it works

What one request does

GET /v1/foods/{id} takes the UUID you got from a search or a barcode lookup and returns the whole normalized record. There is no field selection and no pagination: the record for 62ea12b8-b260-c288-5c38-3a4906f5d6dd (barcode 5449000000996, source openfoodfacts) comes back with every key in the 200 schema, and anything the source lacks is null rather than omitted.

Authenticate with the X-API-Key header. The only optional input is ?lang=, one of en, fr, es, de, it or bg. It appends category_localized, category_segments_localized and allergens_localized and leaves every other key untouched, so adding lang later never breaks an existing parser. Every response also carries an X-Request-Id header to quote in support requests.

Names, ingredients and the main entry

name and ingredients are arrays of {lang, text}. The first entry has lang main and mirrors name_default and ingredients_default; product_lang tells you which real language that is. On the Coca-Cola record the main name is Original Taste, the fr entry reads Coca-Cola en canette and the bg entry reads Кока-Кола. Pick the entry matching your UI language and fall back to name_default.

Expect ingredients to be longer than the number of languages: Open Food Facts stores ingredient text per packaging variant, so the same lang can appear more than once on a record, often with identical text, as it does here. Deduplicate on lang and keep the first occurrence. The text is contributor-entered and returned as-is; the ar entry here contains label fragments that are not ingredients, so validate before parsing it into structures.

Category, tags, allergens and lang

category is one comma-joined string from broad to narrow: Beverages and beverages preparations,Beverages,Carbonated drinks,Sodas,Colas,pt:bebidas cafeína. Split on the comma; the last level is the most specific. categories_tags holds the same levels as machine tags (en:beverages, en:carbonated-drinks, en:colas) and is the stable value to filter or group on, because the display string changes when contributors edit the product.

allergens and traces use the same en:milk style tags, as do countries (en:albania, en:austria, en:belgium) and labels. null means the source stated nothing, not that the product is allergen-free; this record has allergens: null and traces: null, so an allergy warning should read unknown, not none.

With ?lang=bg the API adds category_localized as one translated string, category_segments_localized with one entry per hierarchy level, and allergens_localized parallel to allergens. A level or tag without a dictionary entry is null inside the array, so keep the index aligned with categories_tags or allergens and fall back to the English tag for null slots.

Scores, serving size and summary nutrients

nutriscore is a lowercase letter (e here), nova_group a number (4) and ecoscore_grade a string that can be a letter or a sentinel such as not-applicable, which is what this beverage returns. Treat all three as nullable and do not map not-applicable to a color band.

serving_size_raw and quantity are free text copied from the label. serving_size_raw reads 1 can (330 ml) and is fine for display; quantity reads 0kg on this record, a contributor error, so never compute pack weight from it without a sanity check.

nutrition_summary gives six per-100 g values: energy_kcal, proteins_g, carbs_g, fat_g, sugars_g and fiber_g. Each can be null on its own and the object can be null when the source has no nutrition panel. It covers a list row or a quick comparison; the full 40+ nutrients live on the Nutrition API at /v1/foods/{id}/nutrients with the same id.

Stable ids, caching and the usual flow

The id is deterministic, derived from the source and the source's own product id, so the same product gets the same UUID on every re-import. Store ids in your own tables and re-fetch months later without a remapping step. barcode is nullable in the schema, so prefer the id over the barcode as your join key.

A 200 carries Cache-Control: public, max-age=300 and a 404 carries max-age=60, so a CDN or HTTP cache in front of your service honors them without configuration. If you cache in your own database, key on id plus lang, since the localized keys differ per language.

The usual flow: obtain an id from the Food Search API or the Barcode Lookup API, store it, and call this endpoint when a detail view needs the full record. To refresh many stored ids at once, the Bulk Food Lookup API takes a list in one request on Starter and above. Images are Open Food Facts contributor photos hosted at images.openfoodfacts.org and records from other sources may have none, so render a placeholder whenever image_url is null.

Build with it

Product detail pages

Render a rich food page from one canonical record.

Data enrichment pipelines

Hydrate your catalog with ingredients, allergens, and scores.

Research & analytics

Work with a clean, normalized view across two public datasets.

Limits by plan

Free 100 requests/day, 1/s sustained, burst 5
Starter 5,000 requests/day, 5/s sustained, burst 25
Pro 25,000 requests/day, 20/s sustained, burst 100
Enterprise 100,000 requests/day, 50/s sustained, burst 250

Every response carries X-RateLimit-* headers with the remaining budget; every 429 carries Retry-After. Compare plans →

Errors and how to handle them

401 — The X-API-Key header is missing or the key is not valid.

Send a live key from your dashboard; the body is {error, message} like every other error.

404 — The id is a well-formed UUID but no product has it.

Drop the id from your store; the 404 is cacheable for 60 seconds, so do not retry in a loop.

422 — The id in the path is not a UUID (for example a barcode or a slug).

Resolve barcodes through the Barcode Lookup API and pass only UUIDs here.

429 — The key exceeded its per-second burst or the account used up its daily quota.

Wait the seconds given in Retry-After, then resume; watch X-RateLimit-Second-Remaining and X-RateLimit-Daily-Remaining to back off earlier.

Compared to other APIs

Most food data APIs hand you either a raw Open Food Facts dump or a USDA-only record, each in its own shape. FoodBase returns both sources through one schema, so an openfoodfacts product and a USDA product share the same keys and nullability rules and you write a single parser. Ids are deterministic rather than row numbers, so they survive re-imports, and the record already includes category tags, allergen tags and six summary nutrients. The trade-off is that this is a normalized view, not the upstream editor: contributor errors such as a quantity of 0kg arrive as they are, and the full nutrient panel lives on the Nutrition API rather than in this response.

See the full comparison of food database APIs →

Frequently asked questions

Where do I get an id to pass to this endpoint? +

Every result from the Food Search API and every record from the Barcode Lookup API includes the id. It is derived from the source and its product identifier, so store it once and reuse it across imports.

Why is name an array and what is lang main? +

Products carry names in several languages, so name lists {lang, text} pairs. The entry with lang main mirrors name_default and is the source's primary text; pick the entry matching your UI language and fall back to name_default.

What does ?lang= change? +

It adds category_localized, category_segments_localized and allergens_localized for en, fr, es, de, it or bg. Nothing else in the record changes, and any tag without a translation is null in those fields.

Does allergens: null mean the product has no allergens? +

No. null means the source recorded no allergen data at all, so show an unknown state rather than a safe label. Only a non-empty array is positive information, and even then it reflects what contributors transcribed from the packaging.

Are the nutrients in this response the full panel? +

No. nutrition_summary holds energy_kcal, proteins_g, carbs_g, fat_g, sugars_g and fiber_g per 100 g; the complete set of 40+ nutrients is served by the Nutrition API at /v1/foods/{id}/nutrients using the same id.

Can I cache the record on my side? +

Yes. A 200 is marked Cache-Control: public, max-age=300 and a 404 max-age=60, so any HTTP cache or CDN can hold them; for longer storage, key the record by id and lang and refresh on your own schedule.

Does this endpoint have its own quota? +

No. It draws on the plan's ordinary request quota, which is shared by every key on the account while the per-second limit applies per key. Only the Nutrition API has a separate daily allowance on the Free plan.

Start building with the Food Database API

Free to start. No credit card required.

Get API key Go to Dashboard