# Open Food Facts Product Lookup — Ingredients & Nutri-Score API (`accountable_eel/openfoodfacts-product-lookup`) Actor

Look up food products by barcode against Open Food Facts' free, crowdsourced database of 3M+ products. Get name, brand, ingredients, allergens, Nutri-Score, Eco-Score, NOVA group, and per-100g nutrition facts. Pay only for barcodes that resolve.

- **URL**: https://apify.com/accountable\_eel/openfoodfacts-product-lookup.md
- **Developed by:** [Adrian Voss](https://apify.com/accountable_eel) (community)
- **Categories:** E-commerce, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.40 / 1,000 successful lookups

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Open Food Facts Product Lookup

Look up any food product by barcode (GTIN/UPC/EAN) against the free, crowdsourced
[Open Food Facts](https://world.openfoodfacts.org) database — ingredients, allergens,
Nutri-Score, NOVA processing group, Eco-Score, and full per-100g nutrition facts. No API key
and no scraping: this calls the official public API directly, so you get the same record the
Open Food Facts app reads.

### Who it's for

Built for anyone who needs real product data behind a barcode without a paid database
subscription. Food-app and diet-tracker builders use it to turn a user's barcode scan into
Nutri-Score, NOVA group and full nutrition facts on the spot. Import and retail compliance
teams screen a supplier's SKU list for undeclared allergens and additives before it ever
reaches a shelf. Comparison-shopping and price-tracking tools attach a name, brand and image
to a barcode list pulled from POS or supplier data — the boring enrichment step that turns a
list of numbers into something a shopper recognizes.

### Why this one

- **The official public API, not a scrape.** Open Food Facts publishes a free product API and
  this actor calls it directly — no HTML parsing to break, no key to request.
- **You don't pay for barcodes it doesn't know.** A barcode that isn't in the database, or
  isn't a valid GTIN, comes back `found: false` and is never billed.
- **Everything on the label in one row.** Ingredients, allergens, additives, labels,
  Nutri-Score, NOVA group, Eco-Score and eight per-100g nutrition figures — one lookup, not
  five.
- **Separators are stripped for you.** `0737628064502`, `737628064502` and a barcode pasted
  with spaces or hyphens all resolve to the same product.
- **Stably-named fields.** Column names are frozen between runs, which matters if an AI agent
  or a Clay column reads the schema once and keeps using it.

### What you get

One row per input barcode, with field names frozen between runs — a schema mapped once by an
agent or a Clay column keeps working.

| Field | Type / format | Description |
|---|---|---|
| `query` | text | The barcode exactly as submitted, before separators are stripped. |
| `found` | boolean | Whether the barcode resolved to a product. Gates billing — `false` rows are never charged. |
| `status` | text | `OK`, `NOT_FOUND` (barcode not in the database), or `BAD_FORMAT` (not an 8–14 digit GTIN). |
| `barcode` | text | The normalized barcode as Open Food Facts stores it. |
| `name` | text | Product name, falling back to the generic name when the branded one is blank. |
| `brands` | text | Brand string as entered by contributors — may list several, comma-separated. |
| `categories` | array | Category path, split into separate entries. |
| `quantity` | text | Pack size as printed — e.g. `155 g`. |
| `nutriScore` | text | Nutri-Score grade `A`–`E`, uppercased. `null` when not computed. |
| `novaGroup` | number | NOVA processing group, 1 (unprocessed) to 4 (ultra-processed). |
| `ecoScore` | text | Eco-Score grade, uppercased. Often `UNKNOWN` outside the EU. |
| `ingredientsText` | text | Full ingredients list as printed on the packaging. |
| `allergens` | array | Declared allergens, with the `en:` language prefix stripped. |
| `additives` | array | Additive codes present — e.g. `e330`. |
| `labels` | array | Certifications and claims — `Organic`, `Vegan`, `No gluten`. |
| `countries` | array | Markets the product is recorded as being sold in. |
| `nutrition` | object | Per-100g figures: `energyKcal100g`, `fat100g`, `saturatedFat100g`, `carbohydrates100g`, `sugars100g`, `fiber100g`, `proteins100g`, `salt100g`. |
| `imageUrl` | link | Front-of-pack product photo. |
| `scrapedAt` | date (ISO 8601) | When the lookup ran. |

### Price

$4 per 1,000 barcodes, plus a $0.005 start fee. Misses (`found:false`) are never charged.

You're billed per barcode that resolves to a product. A barcode the database doesn't know, or
one that isn't a valid GTIN, costs nothing no matter how many are in the list. Run 1,000
barcodes and, at the live FREE-tier price, that's roughly **$4 if every one resolves** — cheaper in
practice, since misses cost nothing, and cheaper again on paid Apify tiers. No monthly fee runs
whether or not you use it that month.

### How to use

1. **In the Apify Console.** Open the actor page and click **Start** — the `barcodes` field is already pre-filled with a working example. Results land in the run's dataset as soon as each item is found.
2. **Via the API.** Call it directly with a POST request — no Console needed once you have an API token:
   ```bash
   curl "https://api.apify.com/v2/acts/accountable_eel~openfoodfacts-product-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \
     -X POST \
     -H "Content-Type: application/json" \
     -d '{"barcodes":["0737628064502"]}'
   ```
3. **On a schedule.** Save this actor as an Apify **Task** with the input you want, then add a **Schedule** (hourly, daily, weekly) so it runs on its own — no server of your own required.

1) Paste your barcodes into `barcodes` — digits only, with or without a leading zero.
2) Run the actor. Each barcode is looked up against the Open Food Facts product API, in
   parallel up to `maxConcurrency`.
3) Read the dataset. Each row carries the full product record, or `found: false` if the
   barcode isn't in the database.
4) Turn on `onlyFound` if you want the misses dropped from the output entirely.

**Common ways teams use this:**

- **Screen a catalogue for allergens or additives** before listing or importing it, by
  filtering rows on `allergens` and `additives`.
- **Pull per-100g nutrition in bulk** for a private-label range or a comparison app.
- **Enrich a barcode scan** with Nutri-Score and NOVA group instantly inside a health or diet
  product.
- **Attach names, brands and images** to a barcode list exported from POS or supplier data.
- **Check which markets a product is sold in** using `countries` before planning a launch.

### Input

```json
{
  "barcodes": [
    "0737628064502"
  ]
}
```

One GTIN/UPC/EAN barcode per line, digits only. Accepted formats: 0737628064502, 737628064502.

```json
{
  "barcodes": ["0737628064502", "737628064502"],
  "testRun": false,
  "onlyFound": false,
  "maxConcurrency": 5
}
```

`barcodes` is your list of GTIN/UPC/EAN codes — separators are stripped automatically, so
spaces and hyphens are fine. Turn on `testRun` to try the list against just the first five
barcodes before committing to the full batch. `onlyFound` drops miss rows from the output,
`maxResults` stops the run early once that many products have been found, `includeKeywords`
and `excludeKeywords` filter rows by content, and `columns` picks which fields come back —
useful for keeping a Clay table narrow.

### Sample output

| query | found | status | name | barcode | brands | categories | quantity | nutriScore | novaGroup | ecoScore | ingredientsText | allergens | additives | labels | countries | nutrition | imageUrl | scrapedAt |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 0737628064502 | true | OK | Thai peanut noodle kit includes stir-fry rice noodles & thai peanut seasoning | 0737628064502 | Simply Asia, Thai Kitchen | \["Cereals and their products","Noodles","Rice Noodles"] | 155 g | D | 4 | UNKNOWN | Rice Noodles (rice, water), seasoning packet (peanut, sugar, salt, corn starch, spices \[chili, cinnamon, pepper, cumin, clove], hydrolyzed soy protein, green onions, citric acid, peanut oil, sesame oil, natural flavor). | \["peanuts","sesame seeds","soybeans"] | \["e330"] | \["No gluten","Vegan"] | \["France","United States"] | {"energyKcal100g":385,"fat100g":7.69,"saturatedFat100g":1.92,"carbohydrates100g":71.15,"sugars100g":13.46,"fiber100g":1.9,"proteins100g":9.62,"salt100g":0.72} | https://images.openfoodfacts.org/images/products/073/762/806/4502/front\_en.6.400.jpg | 2026-08-23T21:04:47.132Z |

That table lists every available column. A single row reads like this:

```json
{
  "query": "0737628064502",
  "found": true,
  "status": "OK",
  "barcode": "0737628064502",
  "name": "Thai peanut noodle kit",
  "brands": "Simply Asia, Thai Kitchen",
  "categories": ["Cereals and their products", "Noodles", "Rice Noodles"],
  "quantity": "155 g",
  "nutriScore": "D",
  "novaGroup": 4,
  "ecoScore": "UNKNOWN",
  "allergens": ["peanuts", "sesame seeds", "soybeans"],
  "additives": ["e330"],
  "labels": ["No gluten", "Vegan"],
  "countries": ["France", "United States"],
  "nutrition": { "energyKcal100g": 385, "fat100g": 7.69, "sugars100g": 13.46, "salt100g": 0.72 },
  "scrapedAt": "2026-08-23T21:04:47.132Z"
}
```

A barcode the database doesn't know still returns a row, and is never charged:

```json
{
  "query": "0000000000000",
  "found": false,
  "status": "NOT_FOUND",
  "message": "No product is registered under this barcode in Open Food Facts.",
  "scrapedAt": "2026-08-23T21:04:47.132Z"
}
```

Failed barcodes stay in the dataset by default rather than disappearing — useful for spotting
a batch of transcription errors versus barcodes that genuinely aren't registered. Set
`onlyFound` if you'd rather not see them.

### Use it from Clay, n8n, Make, or an AI agent

This actor runs synchronously over plain HTTP — call it directly from a script, a workflow tool, or an AI agent, no Apify Console needed once you have an API token.

```bash
curl "https://api.apify.com/v2/acts/accountable_eel~openfoodfacts-product-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"barcodes":["0737628064502"]}'
```

**n8n.** Add an HTTP Request node: Method `POST`, URL `https://api.apify.com/v2/acts/accountable_eel~openfoodfacts-product-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>`, Body Content Type `JSON`, JSON Body `{"barcodes":["0737628064502"]}` (swap in an expression from an earlier node for a real value).

**Clay.** Add an "HTTP API" column: Method `POST`, URL `https://api.apify.com/v2/acts/accountable_eel~openfoodfacts-product-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>`, Body `{"barcodes":["{{barcode}}"]}`, mapping the row's barcode into the `barcodes` array.

**MCP.** In Claude, Cursor, or any MCP client with the Apify MCP server, ask for "Open Food Facts API — Barcode to Nutri-Score & Allergens" — the agent will find and run this actor.

### Tips

- **Send the barcode exactly as scanned.** Leading zeros are preserved and separators
  stripped, so you rarely need to normalize a list before running it.
- **Treat `ecoScore` as EU-centric.** It's frequently `UNKNOWN` for products sold outside
  Europe — don't build a filter that silently drops those rows.
- **Use `columns` to keep a Clay table narrow.** The full record is wide; picking six fields
  makes the output far easier to map into a spreadsheet.
- **Check `countries` before trusting a nutrition figure for your market.** The same brand can
  have a different recipe per country, and the record reflects whichever pack was contributed.
- **Run `testRun` first on a big list.** Five barcodes will tell you whether your codes are the
  right format before you commit the whole batch.

### vs. alternatives

| | What it costs | What you get | Trade-off |
|---|---|---|---|
| **This actor** (`openfoodfacts-product-lookup`) | $4 per 1,000 resolved barcodes (less on paid tiers), $0.005 actor start, nothing for a barcode the database doesn't know | The full Open Food Facts record — ingredients, allergens, additives, labels, Nutri-Score, NOVA, Eco-Score and per-100g nutrition — as one flat row | Coverage is only as good as Open Food Facts itself: crowdsourced, strongest in the EU, thinnest on niche and own-brand products |
| **burbn/barcode-lookup** (122 users) | $0.02 per result, $0.01 start | A general barcode resolver across product types, not just food | Five times the per-row price, and broader rather than deeper — no Nutri-Score, NOVA or per-100g nutrition |
| **Open Food Facts' own API** | Free | The same JSON | It is the same JSON. This handles the barcode normalization, the retries, the concurrency ceiling and the flattening, and hands you a table instead of nested objects. |
| **A commercial GS1 / GDSN feed** | Four figures a year and up | Authoritative manufacturer-supplied data with guaranteed coverage | If you need contractual accuracy for regulated labelling, buy the commercial feed. This is a free, crowdsourced source and should be treated as such. |

(Prices for the tools above are their August 2026 FREE-tier list prices — check their own
pages before relying on a straight comparison.)

### Coverage: what's in Open Food Facts

Open Food Facts is crowdsourced — anyone with the app can add or edit a product — which cuts
both ways. It means millions of products are covered for free, but coverage tracks where the
contributor community is strongest, not where products are sold. Packaged goods sold in the
EU, and especially France where the project started, have the most complete records: full
ingredients, all four scores, per-100g nutrition. Products sold only in smaller markets, store
own-brands, and anything niche are more likely to have a thin record or no record at all.

Practically: expect `nutriScore` and `ecoScore` to be `null` more often outside Europe, expect
some barcodes not to resolve at all even for real products, and treat a `found: false` as "not
in this crowdsourced database" rather than "doesn't exist." For products where you need
guaranteed coverage — a specific retailer's own catalogue, for instance — this free source
won't get you there; a commercial GS1 feed will, at commercial prices.

### FAQ

**Do I need an Open Food Facts API key?**
No. The product API is free and public — this actor calls it directly, with no credentials
required on your end.

**How accurate and up to date is the data?**
Open Food Facts is crowdsourced and community-maintained, covering millions of products
worldwide. Coverage and freshness vary by product and region — packaged goods sold in the EU,
especially France where the project started, tend to have the most complete records. Treat it
as a very good free source, not as a manufacturer-certified one.

**What happens if a barcode isn't in the database?**
You still get a row, with `found: false` and `status: NOT_FOUND`, so your output always has one
row per input — and you're never charged for it.

**Can I run this against thousands of barcodes at once?**
Yes. Turn on `testRun` first to sanity-check the list against five barcodes, then run the full
batch. `maxConcurrency` controls how many requests run in parallel.

**Does this only work for US products?**
No — it's a global database. Use `countries` on each result to see which markets a product is
actually recorded as being sold in.

**Why is `nutriScore` or `ecoScore` empty on a product that clearly has one?**
Both are computed by Open Food Facts, and only when enough of the underlying nutrition data has
been contributed. A sparse record produces a `null` grade rather than a guess.

**Is the nutrition data per 100g or per serving?**
Per 100g or 100ml, always — that's what the `100g` suffix on each field means. Serving-size
figures aren't returned, so a per-portion number has to be derived from `quantity`.

**Can I look up a product by name instead of barcode?**
No — this actor is barcode-keyed by design. Open Food Facts does offer text search, but it
returns ranked guesses rather than one definite product, which is the wrong shape for a lookup
that promises exactly one row per input.

**Why do two barcodes for the same product return different data?**
Because they're different pack records. A 200g jar and a 400g jar carry separate GTINs and are
contributed separately, so the nutrition may match while `quantity`, `imageUrl` and even
`labels` differ. Match on `barcode`, not on `name`.

**Does the actor write anything back to Open Food Facts?**
No. It only reads. Nothing about your barcode list, your run, or your account is sent to the
project — and no data is stored between runs on this side either.

**What does `novaGroup` actually mean?**
It's the NOVA food-processing classification, 1 to 4: 1 is unprocessed or minimally processed,
4 is ultra-processed. It's a processing measure, not a health score — `nutriScore` is the one
that grades nutritional quality.

### Related actors

- [GTIN/Barcode Lookup](https://apify.com/accountable_eel/gtin-barcode-lookup) — barcode
  resolution across non-food product categories.
- [Allegro Listing Lookup](https://apify.com/accountable_eel/allegro-listing-lookup) — live
  marketplace price and availability when you have a listing URL rather than a barcode.
- [Shopify Store Lookup](https://apify.com/accountable_eel/shopify-store-lookup) — live product
  catalogues for merchants running their own Shopify storefront.

# Actor input Schema

## `barcodes` (type: `array`):

One GTIN/UPC/EAN barcode per line, digits only. Accepted formats: 0737628064502, 737628064502. You're only charged for the ones we actually find — a miss costs nothing.

## `testRun` (type: `boolean`):

Turn this on to test your input on a small sample before running the full list. Turn it off to process everything.

## `onlyFound` (type: `boolean`):

Only keep rows where something was actually found. Misses are always free, whether or not you show them here.

## `includeKeywords` (type: `array`):

Optional. Only keep results that mention at least one of these words (e.g. a job title, a city, a product name). Leave empty to keep everything.

## `excludeKeywords` (type: `array`):

Optional. Drop any result that mentions one of these words. Leave empty to skip nothing.

## `maxResults` (type: `integer`):

Optional. Stop the run once this many results have been found — useful for a quick, cheap sample. Leave blank for no limit.

## `columns` (type: `array`):

Choose which pieces of information to include in each result row. All are included by default.

## `maxConcurrency` (type: `integer`):

Parallel requests. Keep conservative — this target has no browser fallback, so getting blocked costs more than slow-and-steady.

## `proxyConfiguration` (type: `object`):

Apify Proxy config. Residential recommended for anti-bot-sensitive targets.

## Actor input object example

```json
{
  "barcodes": [
    "0737628064502"
  ],
  "testRun": false,
  "onlyFound": false,
  "includeKeywords": [],
  "excludeKeywords": [],
  "columns": [
    "name",
    "barcode",
    "brands",
    "categories",
    "quantity",
    "nutriScore",
    "novaGroup",
    "ecoScore",
    "ingredientsText",
    "allergens",
    "additives",
    "labels",
    "countries",
    "nutrition",
    "imageUrl"
  ],
  "maxConcurrency": 5,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `results` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "barcodes": [
        "0737628064502"
    ],
    "includeKeywords": [],
    "excludeKeywords": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("accountable_eel/openfoodfacts-product-lookup").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "barcodes": ["0737628064502"],
    "includeKeywords": [],
    "excludeKeywords": [],
}

# Run the Actor and wait for it to finish
run = client.actor("accountable_eel/openfoodfacts-product-lookup").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "barcodes": [
    "0737628064502"
  ],
  "includeKeywords": [],
  "excludeKeywords": []
}' |
apify call accountable_eel/openfoodfacts-product-lookup --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,accountable_eel/openfoodfacts-product-lookup"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/JjjV0kTze1htevbsy/builds/VXigkDP3GtU6KDCJr/openapi.json
