#open-food-facts#off#api#tutorial#barcode

Open Food Facts API tutorial: barcode lookups, contributing back, and the v2 endpoint shape

OFF is the FOSS database for packaged foods. Here's how to query it and contribute to it.

What OFF is

Open Food Facts is a crowdsourced database of packaged foods, originally French and now genuinely global. It’s the FOSS counterpart to USDA Branded — same general purpose (manufacturer label data behind a barcode lookup), different shape, free in a different sense.

It’s keyed on EAN/UPC/GTIN barcodes. As of late 2025 it had crossed 3.5 million products, with the largest coverage in France, Germany, the UK, and Spain, and growing US coverage through the OFF-USA initiative.

The v2 API

Base: https://world.openfoodfacts.org

Barcode lookup

curl "https://world.openfoodfacts.org/api/v2/product/737628064502"

Returns the product as a JSON object with status: 1 if found and status: 0 otherwise.

curl "https://world.openfoodfacts.org/cgi/search.pl?search_terms=oat%20milk&search_simple=1&action=process&json=1&page_size=10"

The search endpoint is the older cgi interface; the v2 search is at /api/v2/search.

curl "https://world.openfoodfacts.org/api/v2/search?categories_tags=plant-milks&page_size=10&fields=product_name,nutriments,brands,countries"

The fields= parameter is critical — without it you get the entire fat product record (often 200+ KB per item).

Country-localised endpoints

world.openfoodfacts.org aggregates everything. Country-specific variants us.openfoodfacts.org, de.openfoodfacts.org, etc. filter to that country’s products and tend to return faster on country-relevant queries.

Response shape (relevant fields)

{
  "status": 1,
  "code": "737628064502",
  "product": {
    "product_name": "Almond milk, original unsweetened",
    "brands": "Califia Farms",
    "categories_tags": ["en:plant-milks", "en:almond-milks", "en:beverages"],
    "nutriments": {
      "energy-kcal_100g": 13,
      "energy-kcal_serving": 30,
      "proteins_100g": 0.4,
      "carbohydrates_100g": 0.4,
      "fat_100g": 1.0,
      "sugars_100g": 0,
      "salt_100g": 0.27
    },
    "serving_size": "240 ml",
    "serving_quantity": 240,
    "image_front_url": "...",
    "ingredients_text": "...",
    "countries": "United States,France",
    "completeness": 0.7375,
    "data_quality_tags": [...]
  }
}

The nutriments field always uses metric (g, kcal). Note _100g and _serving variants of every nutrient.

A working Python lookup

import requests

USER_AGENT = "self-hosted-nutrition/1.0 (editor@selfhostednutrition.org)"

def off_lookup(barcode: str):
    r = requests.get(
        f"https://world.openfoodfacts.org/api/v2/product/{barcode}",
        headers={"User-Agent": USER_AGENT},
        params={"fields": "product_name,brands,nutriments,serving_size,completeness"},
        timeout=10,
    )
    r.raise_for_status()
    data = r.json()
    if data.get('status') != 1:
        return None
    p = data['product']
    n = p.get('nutriments', {})
    return {
        "barcode": data['code'],
        "name": p.get('product_name'),
        "brand": p.get('brands'),
        "kcal_100g": n.get('energy-kcal_100g'),
        "protein_100g": n.get('proteins_100g'),
        "carbs_100g": n.get('carbohydrates_100g'),
        "fat_100g": n.get('fat_100g'),
        "completeness": p.get('completeness'),
    }

if __name__ == "__main__":
    p = off_lookup("737628064502")
    print(p)

Send a meaningful User-Agent. OFF maintainers ask you to identify your software so they can debug bad-actor patterns. A polite User-Agent is roughly the entire OFF rate-limit policy.

Rate limits

OFF doesn’t publish hard rate limits. Their guidance is approximately:

  • Light: any volume (a few hundred per hour).
  • Heavy: under 100/sec sustained on the v2 API, with User-Agent identifying you.
  • Very heavy: switch to the bulk dump.

We have run our personal trackers against OFF for years without rate-limit issues.

Bulk dataset

If you want the whole DB, mongo dump or JSONL is available at:

https://static.openfoodfacts.org/data/openfoodfacts-products.jsonl.gz

It’s about 10 GiB compressed, ~80 GiB uncompressed. Daily refresh. Loadable into Mongo natively, into Postgres with JSONB, or into a search engine like Elasticsearch / Meilisearch.

For self-hosted setups the cleanest pattern is:

  1. Run a nightly download of the JSONL.
  2. Load into Postgres with COPY ... FROM PROGRAM 'gunzip -c ...'.
  3. Index on barcode and product_name.

Contributing back

OFF runs on contributions. If you find a product that isn’t in OFF or is missing macros:

  1. Use the OFF Android app to scan the barcode.
  2. Photograph front, ingredients, nutrition panel.
  3. Confirm the OCR or fill in fields manually.
  4. Submit.

Within 24 hours your contribution is in the database, available to every OFF-backed tracker globally. The OFF community is small enough that maintainers may even thank you on a forum thread.

This is the closest thing FOSS has to a network effect. If you’re using a FOSS tracker and find a gap, contributing is the highest-leverage thing you can do.

Comparing OFF and USDA Branded

DimensionOFFUSDA Branded
CoverageGlobal, weighted EUUS-centric
Quality controlCrowdsourced + autoManufacturer-submitted
Macro completeness~85%~97%
API licenseFree, ODbL dataFree, public domain
Bulk dumpYes, dailyYes, monthly
Update lagHoursMonths
Contributing backAnyoneManufacturers only

For an OFF + USDA Branded combined backend:

  • Search OFF first (better for European brands, faster updates).
  • Fall back to USDA Branded if no OFF hit.
  • Cache results locally for at least a week.

This is what Waistline does. It works.

What OFF doesn’t do

  • Restaurant menus. (Almost) nobody contributes restaurant menus.
  • Recipe-level data. Only labelled products.
  • Per-serving micronutrients beyond what fits on a label panel.

For these, OFF is not the right tool.

References