# Open Food Facts Scraper - Barcode, Nutrition, Allergens (`s-r/openfoodfacts-scraper`) Actor

Look up food products by EAN/UPC barcode or search by category, brand, label, country or Nutri-Score. Returns ingredients, allergens, additives, nutrition per 100g, Nutri-Score, NOVA and Eco-Score from the Open Food Facts open database.

- **URL**: https://apify.com/s-r/openfoodfacts-scraper.md
- **Developed by:** [SR](https://apify.com/s-r) (community)
- **Categories:** E-commerce
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 run start fees

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Open Food Facts Scraper

Look up food products by **barcode**, or search a whole category, brand, label
or country. You get the ingredient list, the declared allergens, the additives,
the nutrition table per 100g, and the three scores people actually filter on:
**Nutri-Score** (A-E), **NOVA** (processing level 1-4) and **Eco-Score**.

Open Food Facts is an open database of food products, contributed the way
Wikipedia is and published under an open licence. This Actor reads its **public
API** rather than scraping the site, so it is fast, stable and returns far more
per product than the rendered page shows.

### The barcode is the point

Every product is addressed by its EAN/UPC. That is an exact join key, not a
fuzzy name match, and it is the same key the retail scrapers return.

So the workflow this Actor is built for is: **scrape prices somewhere, put the
barcodes in here, get the food data back.** No name matching, no guessing which
"Coca-Cola 330ml" is which. Feed it a list of barcodes and every row comes back
keyed on the barcode you supplied.

```
barcodes: ["5000112548167", "737628064502"]
```

That returns Coca-Cola Light (Nutri-Score C, NOVA 4) and a Thai peanut noodle
kit (Nutri-Score D, 385 kcal/100g, allergens: peanuts, sesame seeds, soybeans).

### Or search instead

Leave the barcodes empty and filter:

| Field | Example | What it does |
|---|---|---|
| `categories` | `breakfast-cereals` | Everything in a category (27,046 matches) |
| `brands` | `nestle` | One brand's catalogue |
| `labels` | `organic`, `vegan`, `gluten-free` | Products carrying a label |
| `countries` | `netherlands` | Products sold in a market |
| `stores` | `lidl`, `albert-heijn` | Products a retailer carries |
| `nutrition_grades` | `a` | Only Nutri-Score A products |
| `allergens` | `gluten` | Products declaring an allergen |

Filters combine. `labels: organic` plus `countries: netherlands` returns 4,064
matches. Use the tag form with hyphens, lower case.

### What comes back

Per product:

- **Identity**: `barcode`, `product_name`, `generic_name`, `brands`, `quantity`,
  `serving_size`, `packaging`
- **Composition**: `ingredients_text`, `allergens`, `traces`, `additives`
  (E-numbers), `origins`
- **Scores**: `nutriscore_grade` A-E, `nova_group` 1-4, `ecoscore_grade` A-E
- **Nutrition per 100g**: `energy_kcal_100g`, `fat_100g`,
  `saturated_fat_100g`, `carbohydrates_100g`, `sugars_100g`, `fiber_100g`,
  `proteins_100g`, `salt_100g`, `sodium_100g`
- **Classification**: `categories`, `labels`, `countries`, `stores`
- **Images**: `image_url`, plus separate photos of the ingredient list and the
  nutrition table, which is what you want if you are checking a claim against
  the pack
- **`completeness`**: how filled-in the record is, 0 to 1

Tags arrive from the API language-prefixed (`en:gluten`). They are returned
stripped and readable (`gluten`), so they join cleanly.

### Two honest notes about the data

**`is_stub` tells you when a record is empty.** Open Food Facts contains
barcodes that somebody scanned and nobody ever filled in. Those come back with
`is_stub: true` and a null `product_name` rather than being dropped, because
when you are joining on barcodes there is a real difference between *"this
product is not in the database"* and *"it is in the database but empty"*. The
run summary counts them.

**Coverage varies by country and brand.** This is volunteer-contributed open
data, not a commercial catalogue. European food products, and French ones in
particular, are covered far better than most. A missing product is a gap in the
database, not a failure of the scraper, and the Actor says which of the two it
is: an unknown barcode returns a `not_found` error naming that barcode, never a
row of nulls.

### About the intermittent 503s

Open Food Facts is a non-profit running on donated infrastructure, and its API
**returns HTTP 503 intermittently and without much pattern**. Measured while
building this: `page_size=50` failed on a query where `page_size=100` succeeded
seconds earlier, and page 2 failed on two different network exits right after
page 1 came back fine.

Two things this Actor does about that:

1. **Every request is retried with backoff** (`retries`, default 4). Most 503s
   clear on the next attempt.
2. **A 503 is never parsed as an empty result.** It arrives as a 22 KB HTML
   error page, and a scraper that reads it as JSON would report "this category
   has no products" — a confident wrong answer, on a run that looks successful.
   Non-JSON responses are retried and then reported as an explicit error, with
   the rows already collected returned intact.

If a page fails mid-pagination, you get the rows up to that point plus an error
saying where it stopped. Partial and honest beats complete and wrong.

### Input reference

| Field | Type | Default |
|---|---|---|
| `barcodes` | list of EAN/UPC strings | — |
| `categories` | category tag | `breakfast-cereals` |
| `brands`, `labels`, `countries`, `stores`, `allergens` | tag string | — |
| `nutrition_grades` | `a`-`e` | any |
| `limit` | 1-5000 | 100 |
| `retries` | 1-8 | 4 |

Give at least one barcode, a category, or one filter. An empty input is
rejected with a message rather than walking the whole database.

### Licence and attribution

Open Food Facts data is published under the Open Database License (ODbL). If
you redistribute it you inherit that obligation, including attribution and
share-alike on derived databases. The product photos carry their own licences.
Check the Open Food Facts terms before republishing; using the data internally
for analysis or enrichment is straightforward.

# Actor input Schema

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

Look up specific products by barcode. This is the join path: put the barcodes from your own catalogue in, get ingredients, allergens and nutrition out. Leave empty to search by category or filter instead.

## `categories` (type: `string`):

Category tag to search, for example breakfast-cereals, yogurts, chocolates or sodas. Use the tag form with hyphens.

## `brands` (type: `string`):

Filter by brand tag, for example nestle, lidl or danone.

## `labels` (type: `string`):

Filter by label tag, for example organic, fair-trade, vegan or gluten-free.

## `countries` (type: `string`):

Filter to products sold in a country, for example netherlands, france or united-states.

## `stores` (type: `string`):

Filter by store tag, for example lidl, carrefour or albert-heijn. Contributed data, so coverage varies.

## `nutrition_grades` (type: `string`):

Filter by Nutri-Score grade: a, b, c, d or e.

## `allergens` (type: `string`):

Filter to products declaring an allergen, for example gluten, milk, nuts or soybeans.

## `limit` (type: `integer`):

How many products to return.

## `retries` (type: `integer`):

Open Food Facts returns intermittent 503s. Each request is retried this many times with backoff before it is reported as an error.

## Actor input object example

```json
{
  "barcodes": [
    "5000112548167",
    "737628064502"
  ],
  "categories": "breakfast-cereals",
  "nutrition_grades": "",
  "limit": 100,
  "retries": 4
}
```

# Actor output Schema

## `products` (type: `string`):

One row per food product, keyed on barcode.

## `summary` (type: `string`):

Counts, total matches and coverage of Nutri-Score and nutrition.

## `errors` (type: `string`):

Failures with a code and a redacted message.

# 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": [
        "5000112548167",
        "737628064502"
    ],
    "categories": "breakfast-cereals",
    "limit": 100,
    "retries": 4
};

// Run the Actor and wait for it to finish
const run = await client.actor("s-r/openfoodfacts-scraper").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": [
        "5000112548167",
        "737628064502",
    ],
    "categories": "breakfast-cereals",
    "limit": 100,
    "retries": 4,
}

# Run the Actor and wait for it to finish
run = client.actor("s-r/openfoodfacts-scraper").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": [
    "5000112548167",
    "737628064502"
  ],
  "categories": "breakfast-cereals",
  "limit": 100,
  "retries": 4
}' |
apify call s-r/openfoodfacts-scraper --silent --output-dataset

```

## MCP server setup

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

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/fc4LL2CU1NfQiSJhi/builds/CtCKWDCI2pShbh52M/openapi.json
