Food analysis
Turn food into structured data three ways: a photo of a meal, a photo of a packaged food's nutrition label, or a plain-English description. Every analysis returns the same detection object, so a result round-trips into a correction unchanged.
If you already know the food, look it up under Foods instead.
Image analysis uses the reasoning-based analyzer by default: omit reasoning, or send "reasoning": { "effort": "xhigh" }. Send "reasoning": { "effort": "none" } for the standard analyzer. Both return the same response shape and cost the same credits and rate-limit budget. An analysis can take tens of seconds for a complex meal, so show progress. An image that can't be used fails with one of the image_* codes in Errors; retrying the same image fails the same way.
Each detected food carries its food id and its serving.id. Send them as food_id and serving_id to log the food.
Each operation's description states what it costs — see Credits and pricing.
API key or client token.
Costs 10 credits per successful call.
Analyzes a food photo and returns the detected foods with their nutrition and an aggregated total. The photo can show the food itself or a packaged product — the front of the pack, the ingredient list, or the Nutrition Facts panel all work, and a packaged product comes back as a single detection in the usual result shape. image accepts either an http(s) URL or a base64 data URI. Analysis can take tens of seconds for complex meals. Omit reasoning or set reasoning.effort to xhigh to use the reasoning-based analyzer; set its effort to none to use the standard analyzer. Both modes return the same response shape and use the same rate-limit bucket and credit cost. An optional context note (beta) adds details the photo may not show.
Beta: label reading is in testing — returned nutrition can be incomplete or differ from the printed values, so validate results before relying on them. A photo of nothing but a barcode is rejected; use GET /v1.2/foods/barcode/{barcode} for those. Best results come from sharp, well-lit photos with the food or the complete panel large in the frame; ~1,024 px on the shorter side is plenty, and downsizing huge images lowers latency.
Callable with a client token carrying the food_analysis:write scope.
Your credential, sent as Authorization: Bearer <credential>. Two kinds reach this API, and the prefix tells them apart.
API key (sk-…) — created in the Developer Dashboard, it authenticates your whole account. Use it only where you control the machine — never inside a mobile app or a browser. Every endpoint accepts it.
Client token (ct-…) — a short-lived credential bound to one of your end users, minted from your API key by POST /v1.2/auth/client-tokens on your backend and relayed to the device. Safe to hold on a phone: it expires within 2 hours and acts only as the user it was minted for. Endpoints that open with API key or client token accept one and name the scope it must carry; endpoints that open with API key only refuse it.
The food photo — the food itself or a packaged product's label — as an http(s) URL or a base64 data URI (data:image/jpeg;base64,…). Formats: JPG, PNG, WEBP, and non-animated GIF. Around 1,024 px on the shorter side is enough for reliable results (a recommendation, not a validation rule). A URL must be publicly fetchable server-side — hosts that block hotlinking or require a login cannot be read — and has no enforced size cap, though very large files slow the analysis and can time out. Base64 must be a complete data URI and fit the 5 MB request-body cap, so keep raw images under ~3.5 MB before encoding (base64 inflates by ~33%). Prefer the URL when the image is already hosted.
https://january-food-image-dataset-public.s3.us-east-1.amazonaws.com/salmon-green-beans-potatoes.jpgBeta. A note about the meal from the person or app that took the photo: details the image may not show, such as how it was cooked, oils and sauces, hidden ingredients, how many items, or the brand or restaurant. The analysis takes it into account alongside the photo.
cooked in olive oilThe foods detected in the photo, with per-item and total nutrition.
A name for the meal as a whole. Null on text analyses — the caller already has the words. Corrections preserve a null meal name.
Breakfast BowlPattern: \Sinvalid_request: the image is missing or not an http(s) URL / data URI, or the photo shows nothing but a barcode. image_unreachable, image_corrupt, image_format_unsupported, image_invalid_base64: the image itself could not be used — the message says what to fix, and retrying the same image fails the same way.
The request carried no Authorization header, or the credential in it was rejected.
unauthorized — the header is missing or malformed, or the key is not one we recognise. A valid key belonging to the other API version is 403 forbidden instead.
A client token is rejected in one of three ways, and only the first should be handled automatically:
token_expired— the token is past its TTL. Mint a fresh one from your backend and retry the request once. This is routine and expected once per TTL window.token_invalid— no such token: it was never issued, or it has been purged, which happens shortly after it expires. This code is not an automatic token-refresh signal.token_revoked— the token was revoked, byPOST /v1.2/auth/client-token-revocationsor from the dashboard. The end user signs in again in your app, and your backend decides whether to mint another; a device that just re-mints defeats the revocation.
The credential is valid but is not allowed to make this request. forbidden is the general case — a key issued for the other API version, for example. A client token adds client_token_not_allowed, meaning the endpoint takes only an sk- API key — its description opens with API key only. (in this API: POST /v1.2/auth/client-tokens, POST /v1.2/auth/client-token-revocations, POST /v1.2/chat/completions, POST /v1.2/literature/search, POST /v1.2/extractions, GET /v1.2/extractions/{job_id}, GET /v1.2/patients/{patient_id}/snapshot, and GET /v1.2/credits).
On an endpoint that opens with API key or client token., scope_insufficient means the token was minted without the scope named at the end of that endpoint’s description.
The request body exceeds 5 MB (a transport cap, not an image-quality limit). Keep raw images under ~3.5 MB before base64-encoding, or send a URL instead.
Two failures share this status; use code to choose the response.
rate_limited— a rolling abuse window was reached: the partner-wide v1.2 minute cap, a configured short-window limit, or a client-token limit. Back off and retry;Retry-Aftercarries the wait when Redis reports it.credit_limit_exceeded— this call costs more credits than you have left for the billing period, or they are spent. Never returned to an Enterprise account, whose usage past the allowance is billed as overage instead.
Credits reset at the end of your billing period; GET /v1.2/credits reports that instant as resets_at and remains available when either limit refuses other operations.
The vision model took too long; retry, ideally with a smaller image.
Any other error: the HTTP status plus { code, message }. Retry only rate_limited and the transient 5xx codes.
POST /v1.2/food-analysis/image HTTP/1.1
Host: partners.january.ai
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 175
{
"image": "https://january-food-image-dataset-public.s3.us-east-1.amazonaws.com/salmon-green-beans-potatoes.jpg",
"reasoning": {
"effort": "xhigh"
},
"context": "cooked in olive oil"
}{
"meal_name": "Breakfast Bowl",
"total_nutrients": {
"calories": {
"value": 300,
"unit": "g"
},
"protein": {
"value": 300,
"unit": "g"
},
"carbohydrates": {
"value": 300,
"unit": "g"
},
"net_carbohydrates": {
"value": 300,
"unit": "g"
},
"total_fat": {
"value": 300,
"unit": "g"
},
"trans_fat": {
"value": 300,
"unit": "g"
},
"saturated_fat": {
"value": 300,
"unit": "g"
},
"fiber": {
"value": 300,
"unit": "g"
},
"total_sugars": {
"value": 300,
"unit": "g"
},
"added_sugars": {
"value": 300,
"unit": "g"
},
"cholesterol": {
"value": 300,
"unit": "g"
},
"calcium": {
"value": 300,
"unit": "g"
},
"iron": {
"value": 300,
"unit": "g"
},
"potassium": {
"value": 300,
"unit": "g"
},
"sodium": {
"value": 300,
"unit": "g"
},
"vitamin_d": {
"value": 300,
"unit": "g"
}
},
"detections": [
{
"confidence": "high",
"food": {
"name": "Oatmeal",
"brand_name": null,
"id": "70379835",
"quantity": 0.4,
"serving": {
"id": "34237662",
"quantity": 1,
"unit": "cup",
"weight_grams": 60
},
"nutrients": {
"calories": {
"value": 300,
"unit": "g"
},
"protein": {
"value": 300,
"unit": "g"
},
"carbohydrates": {
"value": 300,
"unit": "g"
},
"net_carbohydrates": {
"value": 300,
"unit": "g"
},
"total_fat": {
"value": 300,
"unit": "g"
},
"trans_fat": {
"value": 300,
"unit": "g"
},
"saturated_fat": {
"value": 300,
"unit": "g"
},
"fiber": {
"value": 300,
"unit": "g"
},
"total_sugars": {
"value": 300,
"unit": "g"
},
"added_sugars": {
"value": 300,
"unit": "g"
},
"cholesterol": {
"value": 300,
"unit": "g"
},
"calcium": {
"value": 300,
"unit": "g"
},
"iron": {
"value": 300,
"unit": "g"
},
"potassium": {
"value": 300,
"unit": "g"
},
"sodium": {
"value": 300,
"unit": "g"
},
"vitamin_d": {
"value": 300,
"unit": "g"
}
}
}
}
]
}API key or client token.
Costs 5 credits per successful call.
Parses free text like "a bowl of oatmeal with honey" into detected foods with quantities and nutrition — the text counterpart of POST /v1.2/food-analysis/image. Text analyses return meal_name: null (the caller already has the words) and grade nothing, so every detection carries confidence: null. For keyword search over the food database, use GET /v1.2/foods.
Callable with a client token carrying the food_analysis:write scope.
Your credential, sent as Authorization: Bearer <credential>. Two kinds reach this API, and the prefix tells them apart.
API key (sk-…) — created in the Developer Dashboard, it authenticates your whole account. Use it only where you control the machine — never inside a mobile app or a browser. Every endpoint accepts it.
Client token (ct-…) — a short-lived credential bound to one of your end users, minted from your API key by POST /v1.2/auth/client-tokens on your backend and relayed to the device. Safe to hold on a phone: it expires within 2 hours and acts only as the user it was minted for. Endpoints that open with API key or client token accept one and name the scope it must carry; endpoints that open with API key only refuse it.
Natural-language description of what was eaten; parsed into detected foods with quantities.
a bowl of oatmeal with honey and a bananaThe foods detected in the text, with per-item and total nutrition.
A name for the meal as a whole. Null on text analyses — the caller already has the words. Corrections preserve a null meal name.
Breakfast BowlPattern: \Sinvalid_request: the text is missing or exceeds 512 characters.
The request carried no Authorization header, or the credential in it was rejected.
unauthorized — the header is missing or malformed, or the key is not one we recognise. A valid key belonging to the other API version is 403 forbidden instead.
A client token is rejected in one of three ways, and only the first should be handled automatically:
token_expired— the token is past its TTL. Mint a fresh one from your backend and retry the request once. This is routine and expected once per TTL window.token_invalid— no such token: it was never issued, or it has been purged, which happens shortly after it expires. This code is not an automatic token-refresh signal.token_revoked— the token was revoked, byPOST /v1.2/auth/client-token-revocationsor from the dashboard. The end user signs in again in your app, and your backend decides whether to mint another; a device that just re-mints defeats the revocation.
The credential is valid but is not allowed to make this request. forbidden is the general case — a key issued for the other API version, for example. A client token adds client_token_not_allowed, meaning the endpoint takes only an sk- API key — its description opens with API key only. (in this API: POST /v1.2/auth/client-tokens, POST /v1.2/auth/client-token-revocations, POST /v1.2/chat/completions, POST /v1.2/literature/search, POST /v1.2/extractions, GET /v1.2/extractions/{job_id}, GET /v1.2/patients/{patient_id}/snapshot, and GET /v1.2/credits).
On an endpoint that opens with API key or client token., scope_insufficient means the token was minted without the scope named at the end of that endpoint’s description.
Two failures share this status; use code to choose the response.
rate_limited— a rolling abuse window was reached: the partner-wide v1.2 minute cap, a configured short-window limit, or a client-token limit. Back off and retry;Retry-Aftercarries the wait when Redis reports it.credit_limit_exceeded— this call costs more credits than you have left for the billing period, or they are spent. Never returned to an Enterprise account, whose usage past the allowance is billed as overage instead.
Credits reset at the end of your billing period; GET /v1.2/credits reports that instant as resets_at and remains available when either limit refuses other operations.
Any other error: the HTTP status plus { code, message }. Retry only rate_limited and the transient 5xx codes.
POST /v1.2/food-analysis/text HTTP/1.1
Host: partners.january.ai
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 52
{
"text": "a bowl of oatmeal with honey and a banana"
}{
"meal_name": "Breakfast Bowl",
"total_nutrients": {
"calories": {
"value": 300,
"unit": "g"
},
"protein": {
"value": 300,
"unit": "g"
},
"carbohydrates": {
"value": 300,
"unit": "g"
},
"net_carbohydrates": {
"value": 300,
"unit": "g"
},
"total_fat": {
"value": 300,
"unit": "g"
},
"trans_fat": {
"value": 300,
"unit": "g"
},
"saturated_fat": {
"value": 300,
"unit": "g"
},
"fiber": {
"value": 300,
"unit": "g"
},
"total_sugars": {
"value": 300,
"unit": "g"
},
"added_sugars": {
"value": 300,
"unit": "g"
},
"cholesterol": {
"value": 300,
"unit": "g"
},
"calcium": {
"value": 300,
"unit": "g"
},
"iron": {
"value": 300,
"unit": "g"
},
"potassium": {
"value": 300,
"unit": "g"
},
"sodium": {
"value": 300,
"unit": "g"
},
"vitamin_d": {
"value": 300,
"unit": "g"
}
},
"detections": [
{
"confidence": "high",
"food": {
"name": "Oatmeal",
"brand_name": null,
"id": "70379835",
"quantity": 0.4,
"serving": {
"id": "34237662",
"quantity": 1,
"unit": "cup",
"weight_grams": 60
},
"nutrients": {
"calories": {
"value": 300,
"unit": "g"
},
"protein": {
"value": 300,
"unit": "g"
},
"carbohydrates": {
"value": 300,
"unit": "g"
},
"net_carbohydrates": {
"value": 300,
"unit": "g"
},
"total_fat": {
"value": 300,
"unit": "g"
},
"trans_fat": {
"value": 300,
"unit": "g"
},
"saturated_fat": {
"value": 300,
"unit": "g"
},
"fiber": {
"value": 300,
"unit": "g"
},
"total_sugars": {
"value": 300,
"unit": "g"
},
"added_sugars": {
"value": 300,
"unit": "g"
},
"cholesterol": {
"value": 300,
"unit": "g"
},
"calcium": {
"value": 300,
"unit": "g"
},
"iron": {
"value": 300,
"unit": "g"
},
"potassium": {
"value": 300,
"unit": "g"
},
"sodium": {
"value": 300,
"unit": "g"
},
"vitamin_d": {
"value": 300,
"unit": "g"
}
}
}
}
]
}API key or client token.
Costs 5 credits per successful call — one is free for every successful POST /v1.2/food-analysis/image in your billing period.
Revises an analysis result. Send back the analysis object exactly as POST /v1.2/food-analysis/image or /text returned it, plus instruction describing the correction; the response is a corrected result with recalculated totals. Adjust portions through instruction ("it was about half of that") rather than editing serving quantities by hand. Nutrient keys a detection omits are filled in as zero automatically, and each detection must carry its selected catalog serving and consumed serving count.
Callable with a client token carrying the food_analysis:write scope.
Your credential, sent as Authorization: Bearer <credential>. Two kinds reach this API, and the prefix tells them apart.
API key (sk-…) — created in the Developer Dashboard, it authenticates your whole account. Use it only where you control the machine — never inside a mobile app or a browser. Every endpoint accepts it.
Client token (ct-…) — a short-lived credential bound to one of your end users, minted from your API key by POST /v1.2/auth/client-tokens on your backend and relayed to the device. Safe to hold on a phone: it expires within 2 hours and acts only as the user it was minted for. Endpoints that open with API key or client token accept one and name the scope it must carry; endpoints that open with API key only refuse it.
Plain-English description of what to correct.
The oatmeal was steel-cut, about 2 cups, and there was no honey.The corrected analysis, with recalculated per-item and total nutrition.
A name for the meal as a whole. Null on text analyses — the caller already has the words. Corrections preserve a null meal name.
Breakfast BowlPattern: \SA field is missing or malformed; the message names the exact detection index and problem.
The request carried no Authorization header, or the credential in it was rejected.
unauthorized — the header is missing or malformed, or the key is not one we recognise. A valid key belonging to the other API version is 403 forbidden instead.
A client token is rejected in one of three ways, and only the first should be handled automatically:
token_expired— the token is past its TTL. Mint a fresh one from your backend and retry the request once. This is routine and expected once per TTL window.token_invalid— no such token: it was never issued, or it has been purged, which happens shortly after it expires. This code is not an automatic token-refresh signal.token_revoked— the token was revoked, byPOST /v1.2/auth/client-token-revocationsor from the dashboard. The end user signs in again in your app, and your backend decides whether to mint another; a device that just re-mints defeats the revocation.
The credential is valid but is not allowed to make this request. forbidden is the general case — a key issued for the other API version, for example. A client token adds client_token_not_allowed, meaning the endpoint takes only an sk- API key — its description opens with API key only. (in this API: POST /v1.2/auth/client-tokens, POST /v1.2/auth/client-token-revocations, POST /v1.2/chat/completions, POST /v1.2/literature/search, POST /v1.2/extractions, GET /v1.2/extractions/{job_id}, GET /v1.2/patients/{patient_id}/snapshot, and GET /v1.2/credits).
On an endpoint that opens with API key or client token., scope_insufficient means the token was minted without the scope named at the end of that endpoint’s description.
Two failures share this status; use code to choose the response.
rate_limited— a rolling abuse window was reached: the partner-wide v1.2 minute cap, a configured short-window limit, or a client-token limit. Back off and retry;Retry-Aftercarries the wait when Redis reports it.credit_limit_exceeded— this call costs more credits than you have left for the billing period, or they are spent. Never returned to an Enterprise account, whose usage past the allowance is billed as overage instead.
Credits reset at the end of your billing period; GET /v1.2/credits reports that instant as resets_at and remains available when either limit refuses other operations.
The vision model took too long; retry.
Any other error: the HTTP status plus { code, message }. Retry only rate_limited and the transient 5xx codes.
POST /v1.2/food-analysis/corrections HTTP/1.1
Host: partners.january.ai
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 569
{
"analysis": {
"meal_name": "Breakfast Bowl",
"total_nutrients": {
"calories": {
"value": 166,
"unit": "kcal"
},
"protein": {
"value": 5.94,
"unit": "g"
},
"carbohydrates": {
"value": 28.1,
"unit": "g"
}
},
"detections": [
{
"confidence": "high",
"food": {
"id": "70379835",
"name": "oatmeal",
"brand_name": null,
"nutrients": {
"calories": {
"value": 166,
"unit": "kcal"
},
"protein": {
"value": 5.94,
"unit": "g"
},
"carbohydrates": {
"value": 28.1,
"unit": "g"
}
},
"quantity": 1,
"serving": {
"id": "34237662",
"quantity": 1,
"unit": "cup",
"weight_grams": null
}
}
}
]
},
"instruction": "There was no honey, and the oatmeal was about half a cup."
}{
"meal_name": "Breakfast Bowl",
"total_nutrients": {
"calories": {
"value": 300,
"unit": "g"
},
"protein": {
"value": 300,
"unit": "g"
},
"carbohydrates": {
"value": 300,
"unit": "g"
},
"net_carbohydrates": {
"value": 300,
"unit": "g"
},
"total_fat": {
"value": 300,
"unit": "g"
},
"trans_fat": {
"value": 300,
"unit": "g"
},
"saturated_fat": {
"value": 300,
"unit": "g"
},
"fiber": {
"value": 300,
"unit": "g"
},
"total_sugars": {
"value": 300,
"unit": "g"
},
"added_sugars": {
"value": 300,
"unit": "g"
},
"cholesterol": {
"value": 300,
"unit": "g"
},
"calcium": {
"value": 300,
"unit": "g"
},
"iron": {
"value": 300,
"unit": "g"
},
"potassium": {
"value": 300,
"unit": "g"
},
"sodium": {
"value": 300,
"unit": "g"
},
"vitamin_d": {
"value": 300,
"unit": "g"
}
},
"detections": [
{
"confidence": "high",
"food": {
"name": "Oatmeal",
"brand_name": null,
"id": "70379835",
"quantity": 0.4,
"serving": {
"id": "34237662",
"quantity": 1,
"unit": "cup",
"weight_grams": 60
},
"nutrients": {
"calories": {
"value": 300,
"unit": "g"
},
"protein": {
"value": 300,
"unit": "g"
},
"carbohydrates": {
"value": 300,
"unit": "g"
},
"net_carbohydrates": {
"value": 300,
"unit": "g"
},
"total_fat": {
"value": 300,
"unit": "g"
},
"trans_fat": {
"value": 300,
"unit": "g"
},
"saturated_fat": {
"value": 300,
"unit": "g"
},
"fiber": {
"value": 300,
"unit": "g"
},
"total_sugars": {
"value": 300,
"unit": "g"
},
"added_sugars": {
"value": 300,
"unit": "g"
},
"cholesterol": {
"value": 300,
"unit": "g"
},
"calcium": {
"value": 300,
"unit": "g"
},
"iron": {
"value": 300,
"unit": "g"
},
"potassium": {
"value": 300,
"unit": "g"
},
"sodium": {
"value": 300,
"unit": "g"
},
"vitamin_d": {
"value": 300,
"unit": "g"
}
}
}
}
]
}Last updated
Was this helpful?