DEV Community

Cover image for 8 Free Food & Nutrition APIs (No Key, Tested 2026)
Alex Spinov
Alex Spinov

Posted on • Originally published at blog.spinov.online

8 Free Food & Nutrition APIs (No Key, Tested 2026)

On July 8, 2026 I looked up a barcode that does not exist. Eight zeros. I sent them to Open Food Facts, the largest open nutrition database on the web, and it answered HTTP 200. Green light. Then I read the body: "status":0, "status_verbose":"no code or invalid code". A success code wrapped around a total miss. Ten seconds of trusting the status line and I would have written that empty result into a calorie tracker as if it were food.

That is the whole post. The list of APIs is the easy part. The hard part is that a keyless food API hands you a clean 200 and a wrong answer, and it does it a slightly different way on almost every endpoint.

A free food API here means a public nutrition, ingredient, or recipe endpoint that returns JSON with no API key, no signup, and no card. Not a CSV dump, not a partner form, not a portal from 2012. A real REST call you can paste into a terminal right now. I found eight that clear that bar, plus three worth knowing that quietly lean on a shared key. I re-verified every one with a live curl on July 8, 2026 (real HTTP code, real body, trimmed but never paraphrased). If you build calorie trackers, meal planners, grocery tools, or an AI agent that answers "how much sugar is in this," these are the lookups you reach for. Every one of them can lie to you with a 200.

Here is the uncomfortable finding before the list. Keyless nutrition data in 2026 is mostly one project. Open Food Facts and its sibling databases (Pet Food, Products, Beauty, Prices) are six of the eight entries below: five distinct databases on one shared engine, with Open Food Facts itself showing up twice because it fails two different ways. Only two entries, Fruityvice and Wger, are independent, and Wger re-imports its data from Open Food Facts anyway. That concentration is not a weakness of the roundup. It is the point. Because it is one engine, the data-quality traps below are systemic, not one-offs. Learn them once and they repeat across the whole family.

Let me be straight about scope, because this is where roundups usually stretch the truth. Full disclosure: I have not run these food APIs in production. My numbers come from a different domain (2,190 scraper runs, incl. 962 on a Trustpilot scraper). I'm citing them only for the pattern: parse the payload, don't trust the status code. The status:0-with-HTTP-200 and unit-mismatch examples below are all from live curl calls I ran on 2026-07-08, not from operating these services at scale.

Here is the full set at a glance.

# API What it answers Example call No key?
1 Open Food Facts Packaged food by barcode GET world.openfoodfacts.org/api/v2/product/<barcode>.json Yes
2 Open Food Facts (units) Per-100g vs per-serving, kJ vs kcal same endpoint, nutriments field Yes
3 Open Pet Food Facts Pet food by barcode GET world.openpetfoodfacts.org/api/v2/product/<barcode>.json Yes
4 Open Products Facts Non-food products by barcode GET world.openproductsfacts.org/api/v2/product/<barcode>.json Yes
5 Open Beauty Facts Cosmetics by barcode GET world.openbeautyfacts.org/api/v2/product/<barcode>.json Yes
6 Open Food Facts Prices Crowd-sourced product prices GET prices.openfoodfacts.org/api/v1/prices?size=1 Yes
7 Fruityvice Nutrition for whole fruits GET fruityvice.com/api/fruit/banana Yes
8 Wger 1.3M+ ingredients + workouts GET wger.de/api/v2/ingredient/?limit=1&language=2 Yes

Three bonus APIs (TheMealDB, TheCocktailDB, USDA FoodData Central) sit at the end with an honest label, because they ride a shared public key rather than being truly keyless.

Open Food Facts and its five siblings: one engine, six entries

This is the keyless nutrition web, most of it. Same codebase, same response shape, same traps in slightly different clothing.

1. Open Food Facts: the barcode lookup that returns 200 on a missing product

Open Food Facts is a crowd-sourced database of packaged food. Give the v2 product endpoint a barcode and it returns the product with a nested nutriments object. This is the anchor of the whole post, so look closely at what it does when the barcode is real and when it is not.

# runnable, read-only
curl "https://world.openfoodfacts.org/api/v2/product/3017624010701.json"   # Nutella
curl "https://world.openfoodfacts.org/api/v2/product/00000000.json"        # garbage
Enter fullscreen mode Exit fullscreen mode
// real barcode 3017624010701:
{"code":"3017624010701","product":{"nutriments":{ ... }},"status":1}

// invalid barcode 00000000:
{"code":"00000000","status":0,"status_verbose":"no code or invalid code"}
Enter fullscreen mode Exit fullscreen mode

Both requests returned HTTP 200. The only thing that separates a hit from a miss is status in the body: 1 means found, 0 means nothing. There is no 404 to catch. It gets worse if you ask for specific fields. When I requested ?fields=nutrition_data_per on the Nutella barcode, I got back {"product":{},"status":1,"status_verbose":"product found"}: status says found, and product is an empty object. A 200, a "found," and no data. If your code does resp.json()["product"]["nutriments"] on that, it throws a KeyError on a request that technically succeeded. Trust the status field, not the status code. Docs at openfoodfacts.github.io/openfoodfacts-server/api.

When to use it: any barcode-to-nutrition lookup for packaged food. It is the front door of the whole ecosystem.

2. Open Food Facts again: the units trap (kJ vs kcal, per-100g vs per-serving)

Yes, this is the same endpoint. It earns a second slot because the nutriments object fails a second, sneakier way, and this one does not throw at all. It just gives you a plausible wrong number. I pulled Coca-Cola, barcode 5449000000996.

# runnable, read-only
curl "https://world.openfoodfacts.org/api/v2/product/5449000000996.json"
Enter fullscreen mode Exit fullscreen mode
{"nutriments":{
  "energy":180,        "energy-kcal":42,
  "energy-kj":180,     "energy-kcal_serving":139,
  "energy-kj_serving":594,
  "carbohydrates":10.6,"carbohydrates_serving":35}}
Enter fullscreen mode Exit fullscreen mode

Read energy and you get 180. Looks like calories. It is not. That 180 is kilojoules; the calorie value is energy-kcal, which is 42. Grab the naive energy field as "calories per 100g" and your tracker is wrong by a factor of about 4.3 on every Coke in the database. The same field lives four ways at once: _100g, _serving, kJ, and kcal. And notice carbohydrates is 10.6 (per 100g) while carbohydrates_serving is 35 (a full can). Mix the basis and you are comparing a mouthful to a bottle. Pick your unit and your basis explicitly, then normalize before you store anything.

When to use it: whenever you actually read nutrient values, which is always. This is where calorie apps quietly ship the wrong number.

3. Open Pet Food Facts: the same engine, a different status code

Open Pet Food Facts is the sibling database for pet food, same v2 product API. I looked up a barcode it does not have.

# runnable, read-only
curl -i "https://world.openpetfoodfacts.org/api/v2/product/3182550716291.json"
Enter fullscreen mode Exit fullscreen mode
// HTTP/2 404
{"code":"3182550716291","status":0,"status_verbose":"product not found"}
Enter fullscreen mode Exit fullscreen mode

Here is the twist that makes hardcoding dangerous. The main Open Food Facts endpoint returned 200 on a not-found product (entry 1). This one, same engine, returned 404 on a not-found product. Two databases, one codebase, two different transport signals for the exact same situation. What is identical across both is the body: status:0. So if you wrote if resp.status_code == 200: assume_found(), it works on pet food and silently breaks on food. The body status field is the one signal that holds across the whole family. Docs at openpetfoodfacts.org.

When to use it: pet-food barcode lookups, and as your reminder that status codes are not consistent even within one project.

4. Open Products Facts: 404 again, so stop hardcoding the status

Open Products Facts covers non-food consumer products. Same API, and it confirms the lesson from entry 3.

# runnable, read-only
curl -i "https://world.openproductsfacts.org/api/v2/product/3661112538014.json"
Enter fullscreen mode Exit fullscreen mode
// HTTP/2 404
{"code":"3661112538014","status":0,"status_verbose":"product not found"}
Enter fullscreen mode Exit fullscreen mode

Another 404, not a 200, on not-found. So now you have concrete proof from three sibling databases that you cannot assume "this API always returns 200" or "this API always returns 404." The transport code depends on which sibling you hit. Read status from the body every time. Docs at openproductsfacts.org.

When to use it: barcode lookups for household and non-food goods.

5. Open Beauty Facts: status 1, and almost nothing in the record

Open Beauty Facts is the cosmetics sibling. It is not food, and I am flagging that plainly, but it belongs here because it shows the family's other failure mode: a "found" record that is nearly empty.

# runnable, read-only
curl "https://world.openbeautyfacts.org/api/v2/product/3600542525701.json"
Enter fullscreen mode Exit fullscreen mode
{"code":"3600542525701",
 "product":{"product_name":"Shampooing"},
 "status":1,"status_verbose":"product found"}
Enter fullscreen mode Exit fullscreen mode

status:1, "product found," and the product is one field: a name, "Shampooing." No ingredients, no detail. This is the sparse-record trap. On crowd-sourced data, "found" means someone scanned a barcode once, not that anyone filled in the fields you need. A completeness check (does the field I want actually exist and is it non-empty) is a separate test from the status check. Docs at openbeautyfacts.org.

When to use it: cosmetics lookups, and as a warning that status:1 is a floor, not a guarantee of complete data.

6. Open Food Facts Prices: a 200 full of nulls

Prices is a newer Open Food Facts service where people log what products cost. Different host, its own v1 API, still keyless.

# runnable, read-only
curl "https://prices.openfoodfacts.org/api/v1/prices?size=1"
Enter fullscreen mode Exit fullscreen mode
{"items":[{"id":1,"product":{
  "product_name":null,"brands":null,
  "nutriscore_grade":null,"categories_tags":[]}}]}
Enter fullscreen mode Exit fullscreen mode

HTTP 200, valid JSON, an items array with a real row. And the joined product object is almost entirely null: no name, no brand, no grade, an empty categories array. The price record exists; the product it points to was never enriched. This is the classic "200, but the fields are empty" case. A null-coalescing default on every joined field is not optional here, it is the difference between a working feature and a page full of "null." Docs at prices.openfoodfacts.org.

When to use it: crowd-sourced grocery-price features, with heavy null guards on the joined product.

The two keyless APIs that aren't Open Food Facts

Out of eight keyless entries, exactly two come from a different codebase. One of them handles errors better than the whole Open Facts family. The other inherits Open Food Facts' data and adds a fresh bug on top.

7. Fruityvice: the one that gets not-found right

Fruityvice returns nutrition for whole fruits, keyed by name. It is small and single-purpose, and it is the counter-example that proves good behavior is possible.

# runnable, read-only
curl "https://www.fruityvice.com/api/fruit/banana"   # 200
curl -i "https://www.fruityvice.com/api/fruit/pizza" # 404
Enter fullscreen mode Exit fullscreen mode
// banana, HTTP 200:
{"name":"Banana","nutritions":{"calories":96,"fat":0.2,
  "sugar":17.2,"carbohydrates":22.0,"protein":1.0}}

// pizza, HTTP 404:
{"error":"Not found"}
Enter fullscreen mode Exit fullscreen mode

A miss returns an honest 404 with {"error":"Not found"}, not a 200 with a hidden flag. Refreshing after five endpoints that lie about status. But it hides a different assumption: the serving basis is never stated. Those numbers are per 100 grams (a banana is not 96 calories, a 100g portion of banana is), and nothing in the payload says so. Bake in the wrong basis and every fruit in your tracker is off. A silent assumption is still a bug; it just fails quietly instead of throwing. Docs at fruityvice.com.

When to use it: fast fruit-nutrition lookups, once you have confirmed and hardcoded the per-100g basis.

8. Wger: 1.3M ingredients, every nutrient a string

Wger is an open-source workout and nutrition manager with a public REST API. The ingredient endpoint is large and keyless. Two calls below: one for the catalog size, one for a single ingredient pinned by id so you get the exact same row I did.

# runnable, read-only
curl "https://wger.de/api/v2/ingredient/?limit=1&language=2"  # English catalog size
curl "https://wger.de/api/v2/ingredient/33208/"               # one ingredient, pinned by id
Enter fullscreen mode Exit fullscreen mode
// list endpoint, count only (results come back unordered, so I pin an id below):
{"count":1358809, "results":[ ... ]}

// ingredient 33208, fully reproducible:
{"name":"Flammekueche","energy":219,
 "protein":"6.100","carbohydrates":"23.100",
 "fiber":null,"is_vegan":null,
 "source_name":"Open Food Facts"}
Enter fullscreen mode Exit fullscreen mode

That count is 1,358,809 English ingredients. Drop language=2 and it jumps to roughly 3 million rows across all languages, so the "how big is it" number depends entirely on the filter you send. Either way it sounds like a rival to Open Food Facts, until you read source_name: "Open Food Facts." Wger re-imports the same data. So it inherits Open Food Facts' gaps (fiber and is_vegan come back null) and adds one of its own: the nutrients are strings. "6.100", not 6.1. Try to add two of those in Python or JavaScript and + concatenates them into "6.10023.100" instead of summing. Cast every nutrient to a number before you do arithmetic. One more trap the list endpoint hides: it returns rows in no guaranteed order, so a bare ?limit=1 hands you a different ingredient on every call, which is why I pinned id 33208 instead of trusting "the first row." This is the downstream lesson: when one dataset re-imports another, the original's defects flow through and the new layer can add fresh ones. Wger also serves a separate workout catalog if you need exercises. Docs at wger.de.

When to use it: ingredient search at volume, or workout data, with a hard cast on every numeric field.

Three bonus food and recipe APIs that need a shared key

These three are genuinely useful and genuinely popular, so people search for them. But they are not clean keyless: TheMealDB and TheCocktailDB run on a public shared test key 1, and USDA uses DEMO_KEY. Those are shared, rate-limited, and not yours. They can throttle or break without warning. Included honestly, with the label attached.

9. TheMealDB: recipes, and the null-that-isn't-an-array trap

TheMealDB is a free recipe database. The public test key 1 is in the path.

# runnable, read-only (shared public test key "1")
curl "https://www.themealdb.com/api/json/v1/1/search.php?s=arrabiata" # hit
curl "https://www.themealdb.com/api/json/v1/1/search.php?s=zzzzzz"    # miss
Enter fullscreen mode Exit fullscreen mode
// hit, HTTP 200:
{"meals":[{"idMeal":"52771","strMeal":"Spicy Arrabiata Penne"}]}

// miss, HTTP 200:
{"meals":null}
Enter fullscreen mode Exit fullscreen mode

Both are HTTP 200. A miss does not return {"meals":[]}. It returns {"meals":null}. That difference is a crash. for m in data["meals"] throws TypeError: 'NoneType' is not iterable in Python, and data.meals.length throws in JavaScript, on a response that succeeded. Guard for null, not just for empty. Docs at themealdb.com/api.php.

When to use it: recipe search and meal ideas, with a null check before you iterate, and a plan for when the shared key gets rate-limited.

10. TheCocktailDB: same engine, same null trap

TheCocktailDB is TheMealDB's drinks counterpart, same company, same behavior, same shared key 1.

# runnable, read-only (shared public test key "1")
curl "https://www.thecocktaildb.com/api/json/v1/1/search.php?s=margarita"
Enter fullscreen mode Exit fullscreen mode
{"drinks":[{"idDrink":"11007","strDrink":"Margarita",
  "strInstructions":"Rub the rim of the glass with the lime slice ..."}]}
Enter fullscreen mode Exit fullscreen mode

A hit returns a drinks array; a miss returns {"drinks":null}, exactly like TheMealDB. If you already wrote a null guard for meals, reuse it verbatim here. Docs at thecocktaildb.com/api.php.

When to use it: cocktail and drink recipes, same null caveat as TheMealDB.

11. USDA FoodData Central: a DEMO_KEY and contradictory metadata

USDA FoodData Central is the US government's authoritative food-composition database. It needs a key, but DEMO_KEY works for a look (shared and rate-limited, the same demo key NASA uses).

# runnable, read-only (shared DEMO_KEY, rate-limited)
curl "https://api.nal.usda.gov/fdc/v1/foods/search?query=cheddar&pageSize=1&api_key=DEMO_KEY"
Enter fullscreen mode Exit fullscreen mode
{"totalHits":20607,"totalPages":20607,
 "foodSearchCriteria":{"numberOfResultsPerPage":50,"pageSize":1},
 "foods":[{"fdcId":2022017,"description":"CHEDDAR"}]}
Enter fullscreen mode Exit fullscreen mode

The data is excellent. The metadata argues with itself. I asked for pageSize=1 and the response echoes "pageSize":1, then reports "numberOfResultsPerPage":50 and "totalPages":20607 in the same object, for 20,607 hits. Those numbers cannot all be true at once. Do not build pagination logic off the meta block blind; trust the length of the foods array you actually received. Get your own free key from fdc.nal.usda.gov/api-guide before you ship anything real.

When to use it: authoritative US nutrient data, with your own key and a skeptical eye on the pagination metadata.

Why the famous food APIs aren't on this list

The roundup stays honest by naming what it left out.

The big commercial names all want a key or an OAuth handshake: Nutritionix, Edamam, Spoonacular, CalorieNinjas, FatSecret. Good products, but none of them clear the "no key, no signup" bar, so they are the paid tier this list exists to route around, not entries in it.

Two Open Food Facts search paths flaked during verification, and I am not going to pretend otherwise. The legacy search at /cgi/search.pl returned HTTP 503 ("Page temporarily unavailable") twice on July 8, so do not build a product on it. The newer Search-a-licious path I tried returned HTTP 404 with {"detail":"Not Found"}, which means the URL has moved; use the current docs rather than any fixed search URL you find in a blog post, including this one.

Honorable keyless mention that is not nutrition: Foodish (foodish-api.com/api/) returns a random food photo, {"image":"https://.../idly3.jpg"}, HTTP 200, no key. Fun for a demo. It is pictures, not data, so it does not count toward the eight.

Parse the payload, not the status code

Finding the endpoint is ten percent of the work. Here is the ninety percent, pulled straight from the failures above.

Assert on a field in the body, not on the HTTP code. Every trap on this page slips past a status check. Open Food Facts returns 200 with status:0 on a bad barcode. Prices returns 200 with a null product. TheMealDB returns 200 with meals:null. A if resp.status_code == 200 gate waves all three through. Check the thing that actually signals success: status == 1, a non-empty result, a field you expect to exist.

Never hardcode "this API always returns 200." The same Open Facts engine returned 200 on a missing food and 404 on a missing pet food. Transport codes are not consistent even inside one project, so read the body every time instead of branching on the code.

Normalize units and types before you store. Coca-Cola's energy is 180 kJ while energy-kcal is 42. Wger hands you "6.100" as a string. Pick a unit and a basis (per-100g or per-serving), cast every number, and do it at the boundary, before the value reaches your database, not after a bug report.

Send a real User-Agent. Open Food Facts asks clients to identify themselves, and the whole family rewards a descriptive agent and polite pacing over a hammering anonymous one. No key does not mean no etiquette.

Here is a five-line guard that folds most of this into one function.

# runnable local — validate the body, not the status code
import requests

def get_nutriment(barcode, field="energy-kcal"):
    r = requests.get(
        f"https://world.openfoodfacts.org/api/v2/product/{barcode}.json",
        headers={"User-Agent": "my-calorie-app/1.0 (you@example.com)"},
        timeout=10,
    )
    r.raise_for_status()                     # catches 404/500, NOT the 200+status:0
    body = r.json()
    if body.get("status") != 1:              # the real success flag lives here
        return None                          # invalid/missing barcode: 200 + status:0
    value = body.get("product", {}).get("nutriments", {}).get(field)
    if not isinstance(value, (int, float)):  # rejects Wger-style "6.100" and null
        return None
    return value

print(get_nutriment("3017624010701"))  # Nutella kcal/100g -> a number
print(get_nutriment("00000000"))       # invalid barcode  -> None, no crash
Enter fullscreen mode Exit fullscreen mode

That raise_for_status line catches the honest failures. The status != 1 line catches the dishonest 200s. The isinstance line catches string and null nutrients. Three checks, and the empty results stop reaching your data.

For context, not a claim of scale: I build web-scraping and data-enrichment tool layers, currently 2,190 runs across 32 actors in production, and the output is always a table of rows that need trustworthy enrichment columns. Company identity was the registry column. Time and dates were the holiday column. Nutrition is this one. Same keyless, no-card layer, different question, and the same failure mode every single time: a 200 that is empty, stale, or misread, never an honest 500.

FAQ

Are food and nutrition APIs really free with no key?
Yes. Open Food Facts and its siblings (Pet Food, Products, Beauty, Prices), plus Fruityvice and Wger, return JSON with no key and no signup. The popular commercial APIs (Nutritionix, Edamam, Spoonacular) do require a key, and TheMealDB, TheCocktailDB, and USDA ride a shared public or demo key, so they are labeled as bonuses here rather than clean keyless.

Which free food API has the most products?
Open Food Facts is the largest keyless option and the source most others copy. Wger reports 1,358,809 ingredients in its English catalog (about 3 million across all languages), but source_name shows it re-imports from Open Food Facts, so it is the same data with an extra serialization quirk (nutrients arrive as strings like "6.100").

Why does Open Food Facts return HTTP 200 for a barcode that does not exist?
Because the request was handled successfully even though nothing was found. The API signals the miss in the body with "status":0 and "status_verbose":"no code or invalid code", not with a 404. Check the status field, not the HTTP status code. Confusingly, the sibling Pet Food and Products databases do return 404 on a miss, which is exactly why you read the body every time.

What is the difference between energy and energy-kcal in Open Food Facts?
energy is in kilojoules and energy-kcal is in kilocalories (the "Calories" on a US label). For Coca-Cola I measured energy at 180 (kJ) and energy-kcal at 42 (kcal). Reading energy as calories overstates the value by roughly 4.3x, so always read energy-kcal for calories and normalize the per-100g versus per-serving basis before storing.

Which free recipe API needs no key?
TheMealDB and TheCocktailDB work through a public shared test key 1 in the URL path, so they are effectively keyless but not your key: shared, rate-limited, and able to break without notice. Both return {"meals":null} or {"drinks":null} on a miss rather than an empty array, so guard for null before you iterate or you will crash a request that returned HTTP 200.

Can I use Open Food Facts data commercially?
The database is under the Open Database License (ODbL), which permits commercial use but carries attribution and share-alike obligations on the database itself. Fruityvice, Wger, and USDA have their own terms. Check the specific license before you ship, because "keyless" and "unrestricted" are not the same thing.


Written by Aleksei Spinov. I build web-scraping and data-enrichment tool layers, currently 2,190 runs across 32 actors in production. Every API above was re-verified with a live curl (real HTTP code, real body) on July 8, 2026 before publishing; responses are trimmed, not paraphrased. I did not run these food APIs in production; the 2,190 runs are a different domain, cited only for the pattern. Drafted with an AI assistant, then fact-checked and edited by me against the raw responses.

Follow for the next keyless API I test for the enrichment layer. And tell me: which food or nutrition API has quietly handed you a clean 200 with a wrong body, and which field finally gave the bug away? I read every comment.

Top comments (0)