> For the complete documentation index, see [llms.txt](https://docs.january.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.january.ai/rest-api/food-scans.md).

# Food Scans

Food recognition, three ways: interpret food into structured data from a meal photo or a plain-English description, and correct the result conversationally. Every scan returns the same detection object, so results round-trip into corrections unchanged. Know the food already? Look it up under foods instead.

## Scan a food or label photo

> Analyzes a food photo and returns the detected foods with their nutrition and an aggregated total. Reading packaged-food labels (Nutrition Facts panels) is coming soon; until then, look packaged foods up by barcode (\`GET /v1.2/foods/barcode/{upc}\`). \`image\` accepts either an http(s) URL or a base64 data URI. Analysis can take tens of seconds for complex meals.

```json
{"openapi":"3.0.0","info":{"title":"January AI - Nutrition Intelligence APIs","version":"1.2"},"tags":[{"name":"food-scans","description":"Food recognition, three ways: interpret food into structured data from a meal photo or a plain-English description, and correct the result conversationally. Every scan returns the same detection object, so results round-trip into corrections unchanged. Know the food already? Look it up under foods instead."}],"servers":[{"url":"https://partners.january.ai"}],"security":[{"api-key":[]}],"components":{"securitySchemes":{"api-key":{"scheme":"bearer","bearerFormat":"JWT","type":"http","description":"Your API key (sk-…), minted through the developer portal."}},"schemas":{"ScanPhotoDto":{"type":"object","properties":{"image":{"type":"string","description":"The meal photo, as an http(s) URL or a base64 data URI (data:image/jpeg;base64,…). Any image of the meal works — a camera photo, a screenshot, or a hosted picture. Formats: JPG, PNG, WEBP, and non-animated GIF. A URL must be publicly fetchable server-side: hosts that block hotlinking or require a login cannot be read. Prefer the URL when the image is already hosted — base64 inflates the payload by ~33%, and request bodies over 5 MB are rejected, so keep raw images under ~3.5 MB when encoding."}},"required":["image"]},"ScanResultDto":{"type":"object","properties":{"meal_name":{"type":"string"},"total_nutrients":{"description":"Aggregated nutrition across all detections.","allOf":[{"$ref":"#/components/schemas/MacroNutrientsDto"}]},"detections":{"description":"Detected foods. Always present — an empty array means nothing was recognized.","type":"array","items":{"$ref":"#/components/schemas/DetectionDto"}}},"required":["detections"]},"MacroNutrientsDto":{"type":"object","properties":{"calories":{"$ref":"#/components/schemas/NutrientAmountDto"},"protein":{"$ref":"#/components/schemas/NutrientAmountDto"},"carbohydrates":{"$ref":"#/components/schemas/NutrientAmountDto"},"net_carbohydrates":{"$ref":"#/components/schemas/NutrientAmountDto"},"total_fat":{"$ref":"#/components/schemas/NutrientAmountDto"},"saturated_fat":{"$ref":"#/components/schemas/NutrientAmountDto"},"fiber":{"$ref":"#/components/schemas/NutrientAmountDto"},"total_sugars":{"$ref":"#/components/schemas/NutrientAmountDto"},"added_sugars":{"$ref":"#/components/schemas/NutrientAmountDto"},"sodium":{"$ref":"#/components/schemas/NutrientAmountDto"}}},"NutrientAmountDto":{"type":"object","properties":{"value":{"type":"number"},"unit":{"type":"string","description":"Canonical across the API: g, mg, kcal, IU."}},"required":["value","unit"]},"DetectionDto":{"type":"object","properties":{"confidence_score":{"type":"string","enum":["high","medium","low"],"description":"Photo scans only; absent on text scans."},"food":{"$ref":"#/components/schemas/DetectionFoodDto"}},"required":["food"]},"DetectionFoodDto":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"},"brand_name":{"type":"string","description":"Empty for generic (non-branded) foods."},"nutrients":{"$ref":"#/components/schemas/MacroNutrientsDto"},"servings":{"description":"Never empty: every detection producer guarantees at least one serving.","type":"array","items":{"$ref":"#/components/schemas/DetectionServingDto"}}},"required":["name","nutrients","servings"]},"DetectionServingDto":{"type":"object","properties":{"id":{"type":"number"},"quantity":{"type":"number"},"unit":{"type":"string"},"selected_quantity":{"type":"number","description":"Quantity the parser selected from the text ('2 cups' → 2); text scans only. Advisory — corrections reads the serving's own quantity."}},"required":["id","unit"]},"ApiErrorDto":{"type":"object","properties":{"message":{"type":"string","description":"A developer-facing explanation of what went wrong and how to fix it."},"code":{"type":"string","description":"A stable machine-readable identifier for the class of failure — build retry logic on this, never on message wording. Current values: invalid_request, unauthorized, forbidden, not_found, not_implemented, payload_too_large, rate_limited, internal_error, upstream_error, service_unavailable, upstream_timeout. Only rate_limited, internal_error, upstream_error, service_unavailable, and upstream_timeout are safe to retry (with backoff) — not_implemented is permanent until the feature ships. New codes may be added over time; treat an unknown code according to its HTTP status class."},"docs_url":{"type":"string","description":"Link to the developer documentation."}},"required":["message","code","docs_url"]}}},"paths":{"/v1.2/food-scans/photo":{"post":{"operationId":"scanPhoto","summary":"Scan a food or label photo","description":"Analyzes a food photo and returns the detected foods with their nutrition and an aggregated total. Reading packaged-food labels (Nutrition Facts panels) is coming soon; until then, look packaged foods up by barcode (`GET /v1.2/foods/barcode/{upc}`). `image` accepts either an http(s) URL or a base64 data URI. Analysis can take tens of seconds for complex meals.","parameters":[{"name":"x-end-user-id","in":"header","description":"Optional: your stable ID for the end user this request acts on behalf of. Opaque to January.","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScanPhotoDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScanResultDto"}}}},"400":{"description":"The image is missing or not an http(s) URL / data URI.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"}}}},"401":{"description":"The Authorization header is missing or the API key is invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"}}}},"413":{"description":"The request body exceeds 5 MB. Keep raw images under ~3.5 MB before base64-encoding.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"}}}},"429":{"description":"Either a rate limit was exceeded (`code: rate_limited`) or the monthly credit allowance is spent (`code: credit_limit_exceeded`). When Retry-After is present, wait that many seconds; a per-day allowance resets 24 hours after the first request in its window. Credit exhaustion carries no Retry-After and retrying does not help — the allowance returns at the start of the next calendar month. Call `GET /v1.2/credits` for the balance and reset date.","headers":{"Retry-After":{"description":"Seconds to wait before retrying; present when the window is known.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"}}}},"504":{"description":"The vision model took too long; retry, ideally with a smaller image.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"}}}},"default":{"description":"Any other error: the HTTP status plus { message, code, docs_url }. Retry only rate_limited and the transient 5xx codes — never not_implemented.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"}}}}},"tags":["food-scans"]}}}}
```

## Scan a meal description

> 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-scans/photo\`. Text scans carry no \`meal\_name\` (the caller already has the words). For keyword search over the food database, use \`GET /v1.2/foods\`.

```json
{"openapi":"3.0.0","info":{"title":"January AI - Nutrition Intelligence APIs","version":"1.2"},"tags":[{"name":"food-scans","description":"Food recognition, three ways: interpret food into structured data from a meal photo or a plain-English description, and correct the result conversationally. Every scan returns the same detection object, so results round-trip into corrections unchanged. Know the food already? Look it up under foods instead."}],"servers":[{"url":"https://partners.january.ai"}],"security":[{"api-key":[]}],"components":{"securitySchemes":{"api-key":{"scheme":"bearer","bearerFormat":"JWT","type":"http","description":"Your API key (sk-…), minted through the developer portal."}},"schemas":{"ScanTextDto":{"type":"object","properties":{"text":{"type":"string","description":"Natural-language description of what was eaten; parsed into detected foods with quantities.","maxLength":512}},"required":["text"]},"ScanResultDto":{"type":"object","properties":{"meal_name":{"type":"string"},"total_nutrients":{"description":"Aggregated nutrition across all detections.","allOf":[{"$ref":"#/components/schemas/MacroNutrientsDto"}]},"detections":{"description":"Detected foods. Always present — an empty array means nothing was recognized.","type":"array","items":{"$ref":"#/components/schemas/DetectionDto"}}},"required":["detections"]},"MacroNutrientsDto":{"type":"object","properties":{"calories":{"$ref":"#/components/schemas/NutrientAmountDto"},"protein":{"$ref":"#/components/schemas/NutrientAmountDto"},"carbohydrates":{"$ref":"#/components/schemas/NutrientAmountDto"},"net_carbohydrates":{"$ref":"#/components/schemas/NutrientAmountDto"},"total_fat":{"$ref":"#/components/schemas/NutrientAmountDto"},"saturated_fat":{"$ref":"#/components/schemas/NutrientAmountDto"},"fiber":{"$ref":"#/components/schemas/NutrientAmountDto"},"total_sugars":{"$ref":"#/components/schemas/NutrientAmountDto"},"added_sugars":{"$ref":"#/components/schemas/NutrientAmountDto"},"sodium":{"$ref":"#/components/schemas/NutrientAmountDto"}}},"NutrientAmountDto":{"type":"object","properties":{"value":{"type":"number"},"unit":{"type":"string","description":"Canonical across the API: g, mg, kcal, IU."}},"required":["value","unit"]},"DetectionDto":{"type":"object","properties":{"confidence_score":{"type":"string","enum":["high","medium","low"],"description":"Photo scans only; absent on text scans."},"food":{"$ref":"#/components/schemas/DetectionFoodDto"}},"required":["food"]},"DetectionFoodDto":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"},"brand_name":{"type":"string","description":"Empty for generic (non-branded) foods."},"nutrients":{"$ref":"#/components/schemas/MacroNutrientsDto"},"servings":{"description":"Never empty: every detection producer guarantees at least one serving.","type":"array","items":{"$ref":"#/components/schemas/DetectionServingDto"}}},"required":["name","nutrients","servings"]},"DetectionServingDto":{"type":"object","properties":{"id":{"type":"number"},"quantity":{"type":"number"},"unit":{"type":"string"},"selected_quantity":{"type":"number","description":"Quantity the parser selected from the text ('2 cups' → 2); text scans only. Advisory — corrections reads the serving's own quantity."}},"required":["id","unit"]},"ApiErrorDto":{"type":"object","properties":{"message":{"type":"string","description":"A developer-facing explanation of what went wrong and how to fix it."},"code":{"type":"string","description":"A stable machine-readable identifier for the class of failure — build retry logic on this, never on message wording. Current values: invalid_request, unauthorized, forbidden, not_found, not_implemented, payload_too_large, rate_limited, internal_error, upstream_error, service_unavailable, upstream_timeout. Only rate_limited, internal_error, upstream_error, service_unavailable, and upstream_timeout are safe to retry (with backoff) — not_implemented is permanent until the feature ships. New codes may be added over time; treat an unknown code according to its HTTP status class."},"docs_url":{"type":"string","description":"Link to the developer documentation."}},"required":["message","code","docs_url"]}}},"paths":{"/v1.2/food-scans/text":{"post":{"operationId":"scanText","summary":"Scan a meal description","description":"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-scans/photo`. Text scans carry no `meal_name` (the caller already has the words). For keyword search over the food database, use `GET /v1.2/foods`.","parameters":[{"name":"x-end-user-id","in":"header","description":"Optional: your stable ID for the end user this request acts on behalf of. Opaque to January.","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScanTextDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScanResultDto"}}}},"400":{"description":"The text is missing or too long.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"}}}},"401":{"description":"The Authorization header is missing or the API key is invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"}}}},"429":{"description":"Either a rate limit was exceeded (`code: rate_limited`) or the monthly credit allowance is spent (`code: credit_limit_exceeded`). When Retry-After is present, wait that many seconds; a per-day allowance resets 24 hours after the first request in its window. Credit exhaustion carries no Retry-After and retrying does not help — the allowance returns at the start of the next calendar month. Call `GET /v1.2/credits` for the balance and reset date.","headers":{"Retry-After":{"description":"Seconds to wait before retrying; present when the window is known.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"}}}},"default":{"description":"Any other error: the HTTP status plus { message, code, docs_url }. Retry only rate_limited and the transient 5xx codes — never not_implemented.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"}}}}},"tags":["food-scans"]}}}}
```

## Correct a scan in plain English

> Revises a scan result. Send back \`meal\_name\` and \`detections\` exactly as a photo or text scan returned them (any label works as \`meal\_name\` for text scans), plus \`user\_input\` describing the correction; the response is a corrected result with recalculated totals. Adjust portions through \`user\_input\` ("it was about half of that") rather than editing serving quantities by hand. Nutrient keys a detection omits are filled in as zero automatically; each detection must carry at least one serving.

```json
{"openapi":"3.0.0","info":{"title":"January AI - Nutrition Intelligence APIs","version":"1.2"},"tags":[{"name":"food-scans","description":"Food recognition, three ways: interpret food into structured data from a meal photo or a plain-English description, and correct the result conversationally. Every scan returns the same detection object, so results round-trip into corrections unchanged. Know the food already? Look it up under foods instead."}],"servers":[{"url":"https://partners.january.ai"}],"security":[{"api-key":[]}],"components":{"securitySchemes":{"api-key":{"scheme":"bearer","bearerFormat":"JWT","type":"http","description":"Your API key (sk-…), minted through the developer portal."}},"schemas":{"CorrectScanDto":{"type":"object","properties":{"meal_name":{"type":"string","description":"The meal name from the scan, when it returned one (photo scans do; text scans don't). Defaults to 'Meal'."},"detections":{"description":"The detections array from a photo or text food scan, exactly as returned. Omitted zero-value nutrient keys are filled in automatically; each detection needs at least one serving.","type":"array","items":{"$ref":"#/components/schemas/DetectionDto"}},"user_input":{"type":"string","description":"Plain-English description of what to correct."}},"required":["detections","user_input"]},"DetectionDto":{"type":"object","properties":{"confidence_score":{"type":"string","enum":["high","medium","low"],"description":"Photo scans only; absent on text scans."},"food":{"$ref":"#/components/schemas/DetectionFoodDto"}},"required":["food"]},"DetectionFoodDto":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"},"brand_name":{"type":"string","description":"Empty for generic (non-branded) foods."},"nutrients":{"$ref":"#/components/schemas/MacroNutrientsDto"},"servings":{"description":"Never empty: every detection producer guarantees at least one serving.","type":"array","items":{"$ref":"#/components/schemas/DetectionServingDto"}}},"required":["name","nutrients","servings"]},"MacroNutrientsDto":{"type":"object","properties":{"calories":{"$ref":"#/components/schemas/NutrientAmountDto"},"protein":{"$ref":"#/components/schemas/NutrientAmountDto"},"carbohydrates":{"$ref":"#/components/schemas/NutrientAmountDto"},"net_carbohydrates":{"$ref":"#/components/schemas/NutrientAmountDto"},"total_fat":{"$ref":"#/components/schemas/NutrientAmountDto"},"saturated_fat":{"$ref":"#/components/schemas/NutrientAmountDto"},"fiber":{"$ref":"#/components/schemas/NutrientAmountDto"},"total_sugars":{"$ref":"#/components/schemas/NutrientAmountDto"},"added_sugars":{"$ref":"#/components/schemas/NutrientAmountDto"},"sodium":{"$ref":"#/components/schemas/NutrientAmountDto"}}},"NutrientAmountDto":{"type":"object","properties":{"value":{"type":"number"},"unit":{"type":"string","description":"Canonical across the API: g, mg, kcal, IU."}},"required":["value","unit"]},"DetectionServingDto":{"type":"object","properties":{"id":{"type":"number"},"quantity":{"type":"number"},"unit":{"type":"string"},"selected_quantity":{"type":"number","description":"Quantity the parser selected from the text ('2 cups' → 2); text scans only. Advisory — corrections reads the serving's own quantity."}},"required":["id","unit"]},"ScanResultDto":{"type":"object","properties":{"meal_name":{"type":"string"},"total_nutrients":{"description":"Aggregated nutrition across all detections.","allOf":[{"$ref":"#/components/schemas/MacroNutrientsDto"}]},"detections":{"description":"Detected foods. Always present — an empty array means nothing was recognized.","type":"array","items":{"$ref":"#/components/schemas/DetectionDto"}}},"required":["detections"]},"ApiErrorDto":{"type":"object","properties":{"message":{"type":"string","description":"A developer-facing explanation of what went wrong and how to fix it."},"code":{"type":"string","description":"A stable machine-readable identifier for the class of failure — build retry logic on this, never on message wording. Current values: invalid_request, unauthorized, forbidden, not_found, not_implemented, payload_too_large, rate_limited, internal_error, upstream_error, service_unavailable, upstream_timeout. Only rate_limited, internal_error, upstream_error, service_unavailable, and upstream_timeout are safe to retry (with backoff) — not_implemented is permanent until the feature ships. New codes may be added over time; treat an unknown code according to its HTTP status class."},"docs_url":{"type":"string","description":"Link to the developer documentation."}},"required":["message","code","docs_url"]}}},"paths":{"/v1.2/food-scans/corrections":{"post":{"operationId":"correctScan","summary":"Correct a scan in plain English","description":"Revises a scan result. Send back `meal_name` and `detections` exactly as a photo or text scan returned them (any label works as `meal_name` for text scans), plus `user_input` describing the correction; the response is a corrected result with recalculated totals. Adjust portions through `user_input` (\"it was about half of that\") rather than editing serving quantities by hand. Nutrient keys a detection omits are filled in as zero automatically; each detection must carry at least one serving.","parameters":[{"name":"x-end-user-id","in":"header","description":"Optional: your stable ID for the end user this request acts on behalf of. Opaque to January.","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CorrectScanDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScanResultDto"}}}},"400":{"description":"A field is missing or a detection is incomplete; the message names the exact detection index and problem.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"}}}},"401":{"description":"The Authorization header is missing or the API key is invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"}}}},"429":{"description":"Either a rate limit was exceeded (`code: rate_limited`) or the monthly credit allowance is spent (`code: credit_limit_exceeded`). When Retry-After is present, wait that many seconds; a per-day allowance resets 24 hours after the first request in its window. Credit exhaustion carries no Retry-After and retrying does not help — the allowance returns at the start of the next calendar month. Call `GET /v1.2/credits` for the balance and reset date.","headers":{"Retry-After":{"description":"Seconds to wait before retrying; present when the window is known.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"}}}},"504":{"description":"The vision model took too long; retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"}}}},"default":{"description":"Any other error: the HTTP status plus { message, code, docs_url }. Retry only rate_limited and the transient 5xx codes — never not_implemented.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"}}}}},"tags":["food-scans"]}}}}
```
