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
Grilled chicken with rice and salad
Lean protein and vegetables balance the rice; moderate sodium from the marinade.
- Grilled chicken breast150 g248 kcal
- Steamed white rice1 cup (158 g)205 kcal
- Green salad with olive oil80 g62 kcal
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
402until you top up. - Top up any time: buy any pack again and it adds to what you have.
Docs
Base URL https://server-api.nutriscan.app/platform/v1. 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.
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 https://server-api.nutriscan.app/platform/v1/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"] }
}'
| Field | Type | Notes |
|---|---|---|
type | string | meal (default), label or text |
image_base64 | string | JPEG, PNG or WebP, up to 10 MB and 50 megapixels (PNG and WebP: 24). Send this or image_url, not both. |
image_url | string | Public https URL. We fetch it once. |
text | string | Up to 2,000 characters. Only with type: text. |
language | string | Language of names and the reason: en, de, fr, es, it, pt, nl, pl, ru, tr, hi, ar. Default en. |
context | object | Optional hints that improve accuracy: meal_time, food_type, cooking_method (list), oil_quantity, consumed_quantity, location, goal, food_preferences. |
stream | boolean | true 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 -N https://server-api.nutriscan.app/platform/v1/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… }
| Event | When | Data |
|---|---|---|
scan.started | At once | The scan id. Keep it: if the connection drops, the scan still finishes and GET /scans/{id} returns it. |
items.detected | About 10 to 15 s | dish_name and items with name and portion. A first look: the final result can rename, add or drop items. |
scan.completed | When done | The full result, the same JSON as without streaming. |
error | Instead of completed | {"status": 422 or 500, "error": {"code", "message"}}, for example no_food_detected. |
: ping | Every 15 s | A 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.
{
"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
| Endpoint | Returns |
|---|---|
GET /scans/{id} | A past scan by its id, for scans made with your account. |
GET /usage | Your 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.
| Status | Code | What to do |
|---|---|---|
| 400 | invalid_request, invalid_image, image_too_large, image_url_unreachable | Fix the request. The message names the field. |
| 401 | invalid_api_key | Check the key and the Bearer prefix. |
| 402 | insufficient_credits | Buy credits on your dashboard. Test keys stop too until you do. |
| 403 | key_revoked, account_suspended | Create a new key on your dashboard, or email support@nutriscan.app. |
| 409 | idempotency_conflict, idempotency_mode_conflict, idempotency_in_progress | Use a new Idempotency-Key for a new body or for the other key mode, or retry shortly. |
| 413 | request_too_large | Send an image of 10 MB or less. |
| 422 | no_food_detected | Nothing to scan. Uses the credits it took (usually 20); the message says how many. |
| 429 | rate_limited, platform_busy | Wait for the Retry-After seconds, then retry. |
| 500 | scan_failed | Retry. No credits used. |
| 503 | platform_unavailable | Retry later. |
Limits
| Image | JPEG, PNG or WebP, up to 10 MB and 50 megapixels (PNG and WebP: 24). HEIC is not supported yet; convert it on the device. |
| Text | 2,000 characters |
| Requests | 60 a minute per account, all keys together. Need more? Email us. |
| Scan time | Up to 95 seconds |
| Keys | Up 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.
- Sign in with your emailWe email you a 6-digit code. Your account is made on the spot.
- Buy creditsPick a pack. Credits never expire.
- 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 startedQuestions first? support@nutriscan.app