For app developers

Food photo in.
Nutrition out.

The meal scan behind the NutriScan app, as one HTTPS call. Send a photo, a pack label or a line of text. Get the dish, every item with its portion, 21 nutrients and a NutriScore from A to E.

  • 3input types
  • 21nutrients per item
  • 12languages
POST /v1/scans  {"type": "meal", "image_url": "…"}

Grilled chicken with rice and salad

Lean protein and vegetables balance the rice; moderate sodium from the marinade.

B
515kcal
51.9gProtein
47.7gCarbs
11.1gFat
  • Grilled chicken breast
    150 g
    248 kcal
  • Steamed white rice
    1 cup (158 g)
    205 kcal
  • Green salad with olive oil
    80 g
    62 kcal
This is the result a test key returns. Same JSON shape as a live scan.

Three ways in, one result

Every input returns the same JSON: meal name, NutriScore with its reason, totals, and each item with portion, grams, macros and micros.

"type": "meal"

Meal photo

A plate, a bowl, a takeaway box. Items and portions come from the photo.

"type": "label"

Pack label

A photo of the nutrition table on a packet, read per pack or per serving.

"type": "text"

Text

"2 eggs and a slice of toast". Useful for voice input or a search box.

Pricing

Credit packs in US dollars. Pay once: credits never expire, and buying again adds to your balance (1 credit = $0.001). A scan uses credits for what it really took: the AI work, web lookups, and items matched from our food database.

Starter

$49 one time

49,000 credits

About 380 to 2,400 scans

  • Credits never expire
  • Test and live keys

Growth

$199 one time

199,000 credits

About 1,500 to 9,900 scans

  • Credits never expire
  • Test and live keys

Scale

$599 one time

599,000 credits

About 4,600 to 29,900 scans

  • Credits never expire
  • Test and live keys
  • What a scan uses: 20 credits minimum. A photo with no food about 20, a short text log about 60, a normal plate about 65, a big plate with 9 items about 130. Every result says credits_used.
  • Free: failed scans, and test-key scans while your account has credits.
  • Starting a scan needs 20 credits left. Below that, live and test keys return 402 until you top up.
  • Top up any time: buy any pack again and it adds to what you have.

Docs

Your API URL is on your dashboard after your first purchase. Every path below starts from it. JSON in, JSON out.

Keys

Your keys live on your dashboard. Your first purchase comes with a test key: ns_test_… returns a fixed sample result for free, so you can build your integration first. Create ns_live_… keys there for real scans; they use your credits. Both stop when your credits run out. Send the key as a bearer token.

Call the API from your server. A key inside a mobile app can be pulled out of the binary and used by anyone.

Header
Authorization: Bearer ns_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Create a scan

POST /scans waits for the result and returns it in the same response. Send image_base64 or image_url for meal and label, and text for text.

curl
curl "$NUTRISCAN_API_URL/scans" \
  -H "Authorization: Bearer $NUTRISCAN_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 7b1c5e0a-order-4411" \
  -d '{
    "type": "meal",
    "image_url": "https://example.com/lunch.jpg",
    "language": "en",
    "context": { "meal_time": "lunch", "cooking_method": ["grilled"] }
  }'
FieldTypeNotes
typestringmeal (default), label or text
image_base64stringJPEG, PNG or WebP, up to 10 MB and 50 megapixels (PNG and WebP: 24). Send this or image_url, not both.
image_urlstringPublic https URL. We fetch it once.
textstringUp to 2,000 characters. Only with type: text.
languagestringLanguage of names and the reason: en, de, fr, es, it, pt, nl, pl, ru, tr, hi, ar. Default en.
contextobjectOptional hints that improve accuracy: meal_time, food_type, cooking_method (list), oil_quantity, consumed_quantity, location, goal, food_preferences.
streambooleantrue sends progress as it happens. See Streaming.

Send an Idempotency-Key header to retry safely. The same key with the same body returns the first result and uses credits once. If that first try failed (402, 500), the same key runs a fresh scan. A scan can take up to 95 seconds, so set your client timeout to at least 100, or use streaming.

Streaming

A big plate takes a minute. Add "stream": true and the same call answers at once with text/event-stream: the items the photo shows arrive after about 10 to 15 seconds, the full result when it is ready. Show the items to your user while the numbers load.

curl
curl -N "$NUTRISCAN_API_URL/scans" \
  -H "Authorization: Bearer $NUTRISCAN_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "type": "meal", "image_url": "https://example.com/lunch.jpg", "stream": true }'

event: scan.started
data: {"id":"scn_8Kd2…","status":"processing"}

event: items.detected
data: {"id":"scn_8Kd2…","dish_name":"Tofu salad bowl","items":[{"name":"Tofu","portion":"100 g"}, …]}

: ping

event: scan.completed
data: { …the same JSON as a normal response… }
EventWhenData
scan.startedAt onceThe scan id. Keep it: if the connection drops, the scan still finishes and GET /scans/{id} returns it.
items.detectedAbout 10 to 15 sdish_name and items with name and portion. A first look: the final result can rename, add or drop items.
scan.completedWhen doneThe full result, the same JSON as without streaming.
errorInstead of completed{"status": 422 or 500, "error": {"code", "message"}}, for example no_food_detected.
: pingEvery 15 sA comment that keeps the connection open. Ignore it.

Errors found before the scan starts (a bad key, a bad body, the rate limit, no credits) come back as a normal JSON error with their status code, not as a stream. A streamed scan can run up to 140 seconds. It uses the same credits as a normal call.

The result

Units are in the field names. Each item says where its numbers came from: food_db (our food database), web or estimate.

200 OK
{
  "id": "scn_Q2mX8rT4vK1pLw9eZb3nYh6A",
  "status": "succeeded",
  "engine_version": "2026.11.0",
  "language": "en",
  "meal": {
    "name": "Grilled chicken with rice and salad",
    "nutri_score": "B",
    "nutri_score_reason": "Lean protein and vegetables balance the rice; moderate sodium from the marinade.",
    "totals": {
      "calories": 515, "protein_g": 51.9, "carbs_g": 47.7, "fat_g": 11.1,
      "fiber_g": 2.2, "sugar_g": 1.6, "saturated_fat_g": 2.3, "trans_fat_g": 0,
      "micros": { "sodium_mg": 457, "potassium_mg": 645, "calcium_mg": 69, "iron_mg": 4.2,
                  "vitamin_c_mg": 14, "vitamin_a_mcg": 204, "omega_3_mg": 100, "…": "13 in all" }
    }
  },
  "items": [
    {
      "name": "Grilled chicken breast",
      "portion": { "value": 150, "unit": "g", "display": "150 g" },
      "grams": 150,
      "calories": 248, "protein_g": 46.5, "carbs_g": 0, "fat_g": 5.4,
      "micros": { "sodium_mg": 410, "…": "…" },
      "source": "food_db"
    }
  ]
}

A live result also has "credits_used": 64 and ends with "usage": {"credits_left": 30760}. Test keys return the same shape with "test_mode": true and "credits_used": 0.

Other endpoints

EndpointReturns
GET /scans/{id}A past scan by its id, for scans made with your account.
GET /usageYour credits: credits_left, credits_balance, credits_held (held by scans running now), credits_purchased, credits_used, scans_charged and can_scan.

Errors

Every error has the same body: {"error": {"code": "insufficient_credits", "message": "…"}}. Branch on code; the message is for people.

StatusCodeWhat to do
400invalid_request, invalid_image, image_too_large, image_url_unreachableFix the request. The message names the field.
401invalid_api_keyCheck the key and the Bearer prefix.
402insufficient_creditsBuy credits on your dashboard. Test keys stop too until you do.
403key_revoked, account_suspendedCreate a new key on your dashboard, or email support@nutriscan.app.
409idempotency_conflict, idempotency_mode_conflict, idempotency_in_progressUse a new Idempotency-Key for a new body or for the other key mode, or retry shortly.
413request_too_largeSend an image of 10 MB or less.
422no_food_detectedNothing to scan. Uses the credits it took (usually 20); the message says how many.
429rate_limited, platform_busyWait for the Retry-After seconds, then retry.
500scan_failedRetry. No credits used.
503platform_unavailableRetry later.

Limits

ImageJPEG, PNG or WebP, up to 10 MB and 50 megapixels (PNG and WebP: 24). HEIC is not supported yet; convert it on the device.
Text2,000 characters
Requests60 a minute per account, all keys together. Need more? Email us.
Scan timeUp to 95 seconds
KeysUp to 10 active keys per account

Your data

We keep the photos and results you send and use them to improve our food database. Do not send photos of people or anything that identifies your users. Results are not medical advice.

Start in two minutes

Everything happens on your dashboard.

  1. Sign in with your emailWe email you a 6-digit code. Your account is made on the spot.
  2. Buy creditsPick a pack. Credits never expire.
  3. Copy your keysA test key comes with your first purchase. Create live keys when you are ready.

Get your API keys

Sign in with your email to buy credits and create keys.

Get started

Questions first? support@nutriscan.app