Bulk Food Lookup API
Fetch up to 100 foods in a single request. Send an array of IDs, get the full records back — ideal for hydrating a meal plan, a shopping cart, or a day's food log without a hundred round-trips.
curl -X POST -H "X-API-Key: food_•••" \
-H "Content-Type: application/json" \
-d '{"ids":["62ea12b8...","2fc13a3b..."]}' \
"https://foodbase.dev/v1/foods/bulk"
{
"data": [
{ "id": "62ea12b8...", "name_default": "Coca-Cola", "brand": "Coca-Cola" },
{ "id": "2fc13a3b...", "name_default": "Nutella", "brand": "Ferrero" }
]
} Prefer to click around? Try the product explorer or the photo playground.
What you get back
ids[]
Send 1–100 food IDs in the request body.
data[]
Full food records, one per matched ID.
Order-preserving
Results come back in the order you requested.
Missing IDs
Unknown IDs are simply omitted — no error for the batch.
Available on the Starter plan and above. 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/bulk" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"ids":["62ea12b8-b260-c288-5c38-3a4906f5d6dd","2fc13a3b-1913-5ab6-d2c7-8c09b56f93bb"]}'
const res = await fetch("https://foodbase.dev/v1/foods/bulk", {
method: "POST",
headers: {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
"ids": [
"62ea12b8-b260-c288-5c38-3a4906f5d6dd",
"2fc13a3b-1913-5ab6-d2c7-8c09b56f93bb"
]
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
import requests
res = requests.post(
"https://foodbase.dev/v1/foods/bulk",
headers={"X-API-Key": "YOUR_API_KEY"},
json={
"ids": [
"62ea12b8-b260-c288-5c38-3a4906f5d6dd",
"2fc13a3b-1913-5ab6-d2c7-8c09b56f93bb"
]
},
timeout=30,
)
res.raise_for_status()
data = res.json()
How it works
The request, step by step
POST /v1/foods/bulk with Content-Type: application/json, your X-API-Key header and a body of the form {"ids": [...]}. ids is the only field: 1 to 100 UUID strings. The ids in the code samples above are live records to test with.
Validation runs before the database is touched and is all-or-nothing. A barcode where a UUID should be, an empty array or 101 entries rejects the whole batch with 422 {"error": "Validation Error", "details": [{"field": "ids.3", "message": ...}]}. There is no lang parameter; pick the language you need from the name and ingredients arrays.
Reading the response
data is an array of FoodItem objects built by the same formatter as the Food Database API, so a parser written for the single record works unchanged, from id, barcode and source through categories_tags, image_url and allergens. Missing source data is null, never an absent key.
nutrition_summary holds energy_kcal, proteins_g, carbs_g, fat_g, sugars_g and fiber_g per 100 g. It is null as a whole only when the product has no nutrient row at all; a row lacking one value shows null for that key alone. Label text such as quantity and serving_size_raw arrives exactly as contributors typed it, so parse defensively.
Missing ids, duplicates and drift
The response can be shorter than the request. There is no per-id status and no 404, so put the returned ids in a Set and diff it against what you sent. An id listed twice comes back as one record, so deduplicate before sending and every one of the 100 slots does work.
Ids are deterministic, derived from the source and its own product identifier, so a product keeps its id across re-imports. If a stored id stops resolving, the product is no longer in the catalog under that identity: flag the row stale and re-resolve it through the Barcode Lookup API or the Food Search API rather than deleting it.
Quota and rate-limit math
One bulk call is one request against the daily quota and one token from the per-second bucket, whatever the number of ids. The middleware order is authentication, per-second limit, plan gate, then daily quota, so a 401, a rate-limit 429 or the Starter 403 never consumes a daily request. A 422 does, because validation runs after the quota is counted; check UUIDs client-side.
Read the headers on every 200: X-RateLimit-Second-Limit, X-RateLimit-Burst-Capacity and X-RateLimit-Second-Remaining for the per-key bucket, X-RateLimit-Account-Limit and X-RateLimit-Account-Remaining for the per-second aggregate across all keys on the account, and X-RateLimit-Daily-Limit, X-RateLimit-Daily-Remaining and X-RateLimit-Daily-Reset (on normal accounts) for the shared daily budget and its midnight UTC reset. The POST carries no Cache-Control header, so nothing is edge-cached for you: store what you fetch.
Syncing a catalog nightly
For a retailer assortment, resolve each new barcode once through the Barcode Lookup API and persist the id next to your SKU. The nightly job then never searches: it reads the stored ids, deduplicates, chunks them into groups of 100, posts each chunk here and checkpoints the last completed chunk so a catalog larger than one night's budget resumes where it stopped.
Pace from the headers rather than a fixed sleep, and watch X-RateLimit-Daily-Remaining so the sync does not starve daytime traffic on the same account. When a view needs the 40+ nutrient panel, call the Nutrition API for that id on demand; it takes one id per request and counts as a request too.
- Dedupe ids, chunk by 100, checkpoint per chunk.
- Diff returned ids against sent ids; mark absentees stale.
- Fetch /nutrients lazily, per product, not in the sync.
Build with it
Meal plans & food logs
Resolve every item in a day's plan with one call.
Cart hydration
Turn a list of saved food IDs into full records efficiently.
Sync jobs
Refresh many cached foods in batches on a schedule.
Limits by plan
| Free | Not available: 403 with required_plan starter (not counted against the 100 requests/day) |
|---|---|
| Starter | 5,000 requests/day, 5/s sustained, burst 25; up to 100 ids per request |
| Pro | 25,000 requests/day, 20/s sustained, burst 100; up to 100 ids per request |
| Enterprise | 100,000 requests/day, 50/s sustained, burst 250; up to 100 ids per request |
Every response carries X-RateLimit-* headers with the remaining budget;
every 429 carries Retry-After.
Compare plans →
Errors and how to handle them
403 — Free account; body has error Plan upgrade required, required_plan starter, current_plan free and an upgrade_url
Upgrade at the upgrade_url, or use GET /v1/foods/{id} on Free
422 — ids is missing, empty, longer than 100 or contains a non-UUID; details[] names the failing position
Validate and dedupe client-side; resolve barcodes through the Barcode Lookup API first
429 — Per-key or account bucket empty, or X-RateLimit-Daily-Remaining reached 0 (body: Daily quota exceeded)
Sleep for Retry-After seconds; for the daily case that is the time to midnight UTC
401 — X-API-Key header missing, or the key is invalid or revoked
Check the header name and the key in the dashboard
503 — The quota or rate-limit store is unreachable; the API fails closed
Retry with backoff; the request was not counted
Compared to other APIs
Against the single-record GET the gain is round-trips and quota: 100 records for one request instead of 100 requests. The GET wins when many clients read the same hot product, because its responses are cacheable for 300 seconds by any HTTP cache and it accepts lang; bulk is uncached and fills your own store. The Food Search API and the Barcode Lookup API turn text or a GTIN into an id; this endpoint turns ids you already hold into records. The Nutrition API goes the other way: the full panel for one product instead of six summary values for up to 100.
Frequently asked questions
Can I send barcodes or search terms instead of ids? +
No. Every entry must be a UUID or the whole batch is rejected with 422. Resolve barcodes through the Barcode Lookup API and names through the Food Search API, store the ids, then use them here.
Does a call with 100 ids cost 100 requests? +
No, it costs one. The daily counter and the per-second bucket each move by one per request regardless of how many ids are inside.
Why is there no lang parameter? +
Bulk returns the raw record without the localized helper fields. The name and ingredients arrays contain every language the contributors provided, so choose the entry client-side; if you need category_localized or allergens_localized, use GET /v1/foods/{id}?lang= for that product.
What does a Free account see? +
A 403 with error Plan upgrade required, required_plan starter and an upgrade_url pointing at the billing page. The gate runs before the daily quota, so those 403s do not eat the 100 requests per day, and GET /v1/foods/{id} stays available.
Can I get the full nutrient breakdown in bulk? +
No. Bulk records carry nutrition_summary only. The 40+ nutrient panel comes from the Nutrition API, one id per request; on Starter and above it has no separate daily cap, on Free it is limited to 25 per day.
Explore more endpoints
Food Search API
Full-text search across 6.1 million food products by name, brand, or category — relevance-ranked, paginated, and fast. Search in any language the label was printed in (Cyrillic included), and pass lang=en|fr|es|de|it|bg for localized names, categories and allergens. One GET request returns clean JSON your app can render immediately.
Learn more →Nutrition API
Go beyond the headline macros. Pull a detailed breakdown of 40+ nutrients per product — energy, protein, carbs, fats, sugars, fiber, sodium, vitamins, and minerals — all per 100g, for any food by ID.
Learn more →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.
Learn more →Start building with the Bulk Food Lookup API
Free to start. No credit card required.
Get API key Go to Dashboard