Food Image Recognition API
Turn a photo into food data. Four vision endpoints cover the whole camera flow: decode a barcode, identify a product from its front, read the nutrition label and ingredients, or analyze a plate of food with portion estimates — matched against 6.1M products, in six languages including Cyrillic.
curl -X POST -H "X-API-Key: food_•••" \
-F "[email protected]" \
"https://foodbase.dev/v1/vision/product"
{
"extracted": {
"name": "Масло краве",
"brand": "Bulgaricum",
"quantity": "250g",
"lang": "bg",
"confidence": 0.95
},
"matches": [
{
"score": 0.95,
"product": {
"id": "c5b352b5-...",
"name_default": "Краве масло 82%"
}
}
],
"cached": false
} Prefer to click around? Try the product explorer or the photo playground.
What you get back
POST /vision/barcode
Decode EAN-13/UPC from a photo and look the product up. Free — 0 credits.
POST /vision/product
Identify a product from its front + match it to the catalog. 3 credits.
POST /vision/label
OCR the nutrition-facts panel and ingredients list. 4 credits.
POST /vision/meal
Identify dishes on a plate with portion and kcal estimates. 5 credits.
confidence
Calibrated 0–1 score on every extraction — decide when to ask the user.
cached
Identical images are served from cache — same answer, instant.
Available on every plan through monthly AI credits — each call has a fixed credit price, and every plan (including Free) comes with a credit allowance. 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/vision/product" \
-H "X-API-Key: YOUR_API_KEY" \
-F "[email protected]"
const form = new FormData();
form.append("image", fileInput.files[0]); // a File/Blob from an <input type="file">
const res = await fetch("https://foodbase.dev/v1/vision/product", {
method: "POST",
headers: { "X-API-Key": "YOUR_API_KEY" },
body: form,
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
import requests
with open("photo.jpg", "rb") as f:
res = requests.post(
"https://foodbase.dev/v1/vision/product",
headers={"X-API-Key": "YOUR_API_KEY"},
files={"image": f},
timeout=30,
)
res.raise_for_status()
data = res.json()
How it works
Which endpoint for which photo
All four operations take one multipart/form-data field named image. The server sniffs the bytes, not the filename: JPEG, PNG or WebP, up to 8 MB, or the upload is rejected before any model runs. Photos are auto-rotated from EXIF and resized to 1024 px on the longest edge, so a full-resolution capture only costs upload time.
POST /v1/vision/barcode decodes EAN-13, EAN-8, UPC-A, UPC-E and Code-128 with plain computer vision and looks the code up exactly like GET /foods/barcode/{code}. The 200 body is { barcode, product } with the full catalog record (id, name_default, brand, nutrition_summary and the rest). No language model runs, so it costs 0 credits and is metered like a data call.
The other three are AI calls. /vision/product reads a front of pack into extracted { name, brand, quantity, lang, confidence } plus ranked matches. /vision/label transcribes a nutrition panel into per-100 g numbers, ingredients_text, allergens and serving. /vision/meal returns items[] of recognized dishes with portion estimates. Each response carries request_id and cached.
- A request without the image field is a 422 Validation Error with details[].field = "image".
Reading a /vision/product response
extracted.lang is the language read on the pack (bg for a Cyrillic label) and drives a language-aware catalog search. confidence runs 0 to 1 and gates that search: the catalog is only queried when confidence is at least 0.3 and a name was read; otherwise matches is empty.
matches holds up to 5 products, best first, each { score, product } in the same full FoodItem shape as a Food Database API record. score derives from confidence and rank, so the top match carries the confidence itself and each next entry scores lower. Keep matches[0].product.id; ids are stable across re-imports.
A miss looks like this staging response to a small product photo: {"matches":[],"extracted":{"lang":"","name":"","brand":"","quantity":"","confidence":0},"request_id":"bcca8231-f335-4468-81bf-e587b614d9a4","cached":true}. Empty strings and confidence 0 mean no name could be read; cached: true means the same bytes had been seen before, and the call was still charged 3 credits.
Labels and meals: read versus estimated
/vision/label is transcription, not inference. energy_kcal, fat_g, saturated_fat_g, carbs_g, sugars_g, fiber_g, proteins_g and salt_g are read as printed, and anything not on the label comes back null, never guessed. A readable barcode in the same photo is returned as extracted.barcode and looked up: a hit fills product with the full FoodItem, otherwise product is null.
/vision/meal is the opposite, and its disclaimer field says so. Each item has dish, portion_g_est and confidence, plus match when the dish resolved to a USDA generic food: id, name_default, per_100g { energy_kcal, proteins_g, carbs_g, fat_g } and estimated_kcal, which is per_100g.energy_kcal times portion_g_est over 100. estimated_total_kcal sums the items that have one and is null when none does. Matching uses English generic foods, so match.name_default is English whatever the photo.
- Show the disclaimer text to end users of meal estimates; a dish with match: null still costs the 5 credits.
Credits, refunds and the vision rate limit
Credits are deducted before inference: 3 for product, 4 for label, 5 for meal. The monthly allowance is shared by all keys of the account (Free 100, Starter 5,000, Pro 15,000, Enterprise 40,000) and resets on the first of the UTC month. AI responses carry X-Credits-Limit, X-Credits-Remaining and X-Credits-Reset; GET /v1/credits returns the balance for free.
Running out is a 402, not a 429: { "error": "Insufficient credits", "message": "This call costs 3 credits and your free plan's monthly allowance of 100 is exhausted. Credits reset on 2026-10-01." }. Refunds happen only when the request never reached an answer: rejected image (413 or 422), missing field (422), provider failure (502, message ends "You were not charged.") or vision disabled (503). Cache hits are charged full price on purpose.
AI calls have their own per-second limit, with no burst: 1/s on Free and Starter, 2/s on Pro, 5/s on Enterprise per key, and three times that across the account. Responses carry X-RateLimit-Vision-Limit and X-RateLimit-Vision-Remaining; exceeding it is a 429 with Retry-After: 1 and no charge, because the limiter runs before the deduction.
The scan flow and the photo
Cheapest first. Decode the barcode on-device and call the Barcode Lookup API, or let /vision/barcode decode it for 0 credits. Only when there is no barcode, or the lookup returns 404, send the front of the pack to /vision/product. With matches[0].product.id, fetch the per-100 g breakdown from the Nutrition API or a plain-language verdict from the AI Food Analysis API.
Photo quality decides confidence. For /vision/product frame the front of the pack with name, brand and pack size readable and nothing else in shot. For /vision/label lay the panel flat, fill the frame with the nutrition table and the ingredients paragraph, and include the barcode if it fits. For /vision/barcode the server's own 422 message is the rule: fill the frame with the barcode, avoid glare, keep it in focus.
- Retry a 502 after a short wait; you were refunded. Do not retry a low-confidence 200; retake instead.
Build with it
Photo food logging
Snap a product or a meal and log it — no typing, no barcode hunting.
Label capture
Digitize nutrition facts and ingredients from any package, even products not yet in a database.
Smart scanner fallback
Barcode unreadable? Fall back to front-of-pack recognition in the same flow.
Limits by plan
| Free | 100 AI credits/month; 1 AI request/s per key; /vision/barcode counts against 100 requests/day |
|---|---|
| Starter | 5,000 AI credits/month; 1 AI request/s per key; /vision/barcode counts against 5,000 requests/day |
| Pro | 15,000 AI credits/month; 2 AI requests/s per key; /vision/barcode counts against 25,000 requests/day |
| Enterprise | 40,000 AI credits/month; 5 AI requests/s per key; /vision/barcode counts against 100,000 requests/day |
Every response carries X-RateLimit-* headers with the remaining budget;
every 429 carries Retry-After.
Compare plans →
Errors and how to handle them
402 — Monthly AI credits exhausted; the body names the price, your allowance and the reset date.
Wait for X-Credits-Reset or upgrade. /vision/barcode still works.
404 — /vision/barcode decoded a code that is not in the catalog; the body returns it.
Do not retry. Fall back to /vision/product with a front-of-pack photo.
413 — Upload larger than 8 MB.
Downscale client-side; the server resizes to 1024 px anyway. Credits refunded.
422 — Image missing, undecodable, wrong format, or no barcode in the frame.
Read details[] or message and retake. Credits refunded.
429 — Vision per-second limit exceeded for the key or the account.
Wait Retry-After (1 second) and resend. Nothing was charged.
502 — The vision provider failed mid-call.
Retry once after a pause; the message confirms you were not charged.
503 — Vision not configured, or the limiter or credit store is unreachable.
Retry with backoff. Nothing was charged.
Compared to other APIs
Most photo-to-food services return a label for what they think they see and leave you to find the product. FoodBase resolves in the same call: extraction plus ranked matches against the 6.1M-product catalog, or a linked record when a label photo carries a barcode. It keeps reading and guessing apart: label values are transcribed or null, and only /vision/meal estimates, with a disclaimer field. The trade-off is that a poor photo yields an empty match list and still costs its credits, so photo guidance in your UI pays for itself.
Frequently asked questions
Does /vision/product cost credits when nothing matches? +
Yes. Credits are deducted before inference, and a 200 with matches: [] is a completed call. Retake the photo rather than resend it; a resend is a cache hit at full price.
Why was a cached response still charged? +
Cache hits deliberately cost the same as fresh calls; the cache makes repeats fast and consistent, not free. Deduplicate identical photos client-side before a second upload.
Can /vision/label fill in values the label does not show? +
No. It transcribes the panel and returns null for anything not printed. For an interpretation of those numbers, send the linked product id to the AI Food Analysis API.
Is the vision rate limit the same as my API rate limit? +
No. AI calls use a separate, lower bucket: 1/s on Free and Starter, 2/s on Pro, 5/s on Enterprise per key, and three times that across all keys. Only /vision/barcode uses the regular data-API limit and daily quota.
How do I check remaining credits without spending any? +
Call GET /v1/credits with your key. It returns plan, monthly allowance, credits used and the reset date, and counts against neither credits nor the daily quota.
Explore more endpoints
AI Food Analysis API
One call turns a product's raw data into something you can show a user: a plain-language summary, factual health highlights and warnings, diet compatibility (vegan, vegetarian, gluten-free, lactose-free), and a NOVA-style processing level — in six languages, grounded strictly in the product's label data.
Learn more →Barcode Lookup API
Scan or type a barcode, get the product. Resolve any EAN-13 or UPC code to a food record — brand, category, Nutri-Score, and a nutrition summary — in a single request. Built for scanner-based apps.
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 →Start building with the Food Image Recognition API
Free to start. No credit card required.
Get API key Go to Dashboard