API reference
kcal is five endpoints under https://kcal.elbantli.com/v1. You send an API key with each request, and every response is JSON.
Only successful queries are charged: a response with a 2xx status. Each endpoint's price is below and on the pricing table.
The example responses are shortened. An … marks what is left out.
For a coding assistant, the same reference is one Markdown file: llms-full.txt.
Authentication
Create an API key in the console settings and send it as a Bearer token. The full key is shown once, when you create it.
Keep keys on your server. Anyone who holds a key can spend your credits, so revoke a key you think is exposed.
An account can run 60 queries per minute across all its keys. Past that, requests return a 429 until the minute is over.
Authorization: Bearer $KCAL_API_KEY
GET /v1/basic-search · $0.003 per query
Basic search
Finds foods by name, brand or barcode and returns the closest matches first.
Query parameters
qstring, required- A food or product name, or a barcode. 1 to 512 bytes of UTF-8.
nb_resultsinteger- How many foods to return, from 1 to 12. Defaults to 5.
Works as you type: when the words so far match few foods, the last word also matches the start of longer words. Among strong matches, generic foods come before branded products.
The query is matched as written. Smart search translates other languages and corrects spelling.
The list can be shorter than nb_results, or empty. An empty list is still a successful query.
curl "https://kcal.elbantli.com/v1/basic-search?q=greek+yogurt&nb_results=1" \ -H "Authorization: Bearer $KCAL_API_KEY"
{ "results": [{ "kind": "survey", "name": "Yogurt, Greek, whole milk, plain", "brand": "", "barcode": "", "relaxed": false, "relevance": 0.97, "nutrition": [{ "key": "foodNutrients", "preparation": "as_sold", "basis": { "quantity": 100, "unit": "g" }, "nutrients": { "kcal": 94, "proteinG": 8.78, "carbsG": 4.75, "fatG": 4.39, "sugarsG": 3.25, "saltG": 0.085, "fiberG": 0, "micronutrients": { "vitamin_b12_ug": 0.75, "vitamin_c_mg": 0 } }, "portion": null, "estimated": false, "issues": [] }], "portions": [{ "key": "foodPortions/0", "preparation": "as_sold", "label": "1 cup", "quantity": 1, "unit": "cup", "grams": 245, "millilitres": null }, …] }] }
POST /v1/smart-search · $0.040 per query
Smart search
Splits a meal description into up to 12 foods, reads the amount given for each one, and matches each food to the catalog.
JSON body
qstring, required- The meal in plain language. 1 to 2,048 bytes of UTF-8. The whole body can be at most 16 KiB.
Each item has a status. Only matched items carry a food. The others have no nutrition, so never count them as zero.
Nutrients are not scaled to the serving. Use a portion of the matched food to convert the serving to grams or millilitres first.
curl "https://kcal.elbantli.com/v1/smart-search" \ -H "Authorization: Bearer $KCAL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"q": "2 eggs and a slice of sourdough"}'
{ "results": [{ "text": "2 eggs", "food": "eggs", "serving": { "quantity": 2, "unit": "count", "estimated": false }, "references": ["egg"], "name": "Eggs", … "status": "matched" }, { "text": "a slice of sourdough", "food": "sourdough", "serving": { "quantity": 1, "unit": "slice", "estimated": false }, "references": ["sourdough bread"], "kind": "branded", "name": "SOURDOUGH BREAD, SOURDOUGH", "brand": "HEARTLAND GRAINS", "barcode": "074175001261", "relaxed": false, "relevance": 0.95, "nutrition": [{ "key": "labelNutrients", "preparation": "as_sold", "basis": { "quantity": 1, "unit": "portion" }, "nutrients": { "kcal": 120, "proteinG": 4, "carbsG": 23, "fatG": 1.5, "sugarsG": 2, "saltG": 0.55, "fiberG": 0.989, "micronutrients": {} }, "portion": { "key": "serving", "preparation": "as_sold", "label": "1 SLICE", "quantity": 1, "unit": "SLICE", "grams": 43, "millilitres": null }, "estimated": false, "issues": [] }, …], "portions": [ … ], "status": "matched" }] }
POST /v1/image-transcribe · $0.055 per query
Image search
Identifies up to 12 foods in a photo, estimates each visible portion, and matches each food to the catalog.
Multipart form fields
imagefile- A JPEG or PNG of at most 25 million pixels. The whole request can be at most 10 MiB.
image_urlstring- A public HTTP or HTTPS image URL without credentials, at most 2,048 bytes.
Send exactly one of image and image_url. The response has the same shape as smart search.
A portion estimated from the photo comes back in grams or millilitres with serving.estimated set to true. Show it to your user for review.
Only visible foods are reported. Hidden ingredients are not guessed.
Each item has a status. Only matched items carry a food. The others have no nutrition, so never count them as zero.
Nutrients are not scaled to the serving. Use a portion of the matched food to convert the serving to grams or millilitres first.
curl "https://kcal.elbantli.com/v1/image-transcribe" \ -H "Authorization: Bearer $KCAL_API_KEY" \ -F image=@lunch.jpg
GET /v1/barcode · $0.002 per query
Barcode lookup
Returns the packaged food with exactly this barcode.
Query parameters
barcodestring, required- The barcode as printed. Leading zeros count.
When the catalog has no food for a barcode, the newest food submitted for it is returned. Its id starts with user: and its values are as they were submitted.
A barcode with no food at all returns a 404 with the code not_found, and is not charged.
curl "https://kcal.elbantli.com/v1/barcode?barcode=3017620422003" \ -H "Authorization: Bearer $KCAL_API_KEY"
{ "food": { "kind": "branded", "name": "Nutella", "brand": "Nutella, Ferrero", "barcode": "3017620422003", "relaxed": false, "relevance": 1, "nutrition": [ … ], "portions": [ … ] } }
POST /v1/submit-food · $0.002 per query
Food submission
Stores a packaged food for a barcode the catalog does not have, so that the next lookup of that barcode returns it.
JSON body
idstring, required- A UUID that identifies this submission. The whole body can be at most 16 KiB.
barcodestring, required- The barcode as printed, 8 to 14 digits. Leading zeros count.
namestring, required- The product's name. 1 to 200 characters.
brandstring- The brand. At most 200 characters.
basisobject, required- The amount the nutrients are measured on: a quantity above 0 and a unit of g, ml or portion. With portion, the quantity counts servings and is normally 1.
nutrientsobject, required- kcal, proteinG, carbsG and fatG are required. sugarsG, fiberG and saltG can be left out or null. Energy is in kcal and everything else in grams, as numbers of 0 or more.
servingobject or null- The size of one serving: grams, millilitres or both, each a number above 0 or null.
linkedIdstring- The id of the catalog food these values were copied from, when there is one. At most 64 characters.
Generate the id yourself. Sending an id again stores nothing and returns 200 instead of 201, so a request that timed out is safe to repeat.
A food in the catalog is never replaced: barcode lookup returns a submitted food only when the catalog has none for that barcode. Basic search does not return submitted foods.
Values no food can have are refused with invalid_request: more than 10 kcal per gram, more of a nutrient than the basis weighs, or more sugars than carbohydrates.
An account can submit 1,000 foods in 24 hours. Past that, requests return a 429.
curl "https://kcal.elbantli.com/v1/submit-food" \ -H "Authorization: Bearer $KCAL_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "id": "5e0c1c1e-6f0e-4d53-9d0b-0f3a2a0f8a11", "barcode": "3017620422003", "name": "Nutella", "brand": "Ferrero", "basis": { "quantity": 100, "unit": "g" }, "nutrients": { "kcal": 539, "proteinG": 6.3, "carbsG": 57.5, "fatG": 30.9, "sugarsG": 56.3, "fiberG": null, "saltG": 0.107 }, "serving": { "grams": 15 } }'
{ "id": "user:3017620422003" }
Food object
Basic search, barcode lookup and matched meal items return foods in this shape.
Food
idstring- The food's catalog ID. It stays the same between queries.
kindstring- branded for a packaged product. foundation, survey or legacy for a generic food.
namestring- The food's name.
brandstring- The brand, or an empty string for a generic food.
barcodestring- The barcode, or an empty string when the record has none.
relaxedboolean- true when the food was found by leaving out one of your search terms. Treat it as a suggestion.
relevancenumber- How well the food fits the query, from 0 to 1. It ranks results; it is not a probability.
nutritionarray- Every nutrition profile the source has, for example per 100 g and per serving. Empty when it has none.
portionsarray- The food's serving sizes. Empty when the source has none.
Nutrition profile
keystring- Identifies the profile within the food.
preparationstring- as_sold, prepared or unknown.
basisobject- The amount the nutrients are measured on: a quantity and a unit of g, ml, portion or unknown. A portion basis refers to the profile's portion.
nutrientsobject- kcal, proteinG, carbsG, fatG, sugarsG, saltG and fiberG. Each is a number, or null when the source does not list it. micronutrients maps names that include the unit, such as vitamin_c_mg, to numbers.
portionobject or null- The serving this profile was reported for, when it has one.
estimatedboolean- true when the values are the source's estimate, not a label or measurement.
issuesarray of strings- Problems found in the source record, such as a nutrient with an unsupported unit.
Portion
keystring- Identifies the portion within the food.
preparationstring- as_sold, prepared or unknown.
labelstring- The serving as the source wrote it, such as 1 cup.
quantity, unitnumber or null, string- The serving as a number and a unit, when the source gives them or the label could be parsed.
grams, millilitresnumber or null- The portion's weight or volume. One is never derived from the other.
Nutrients always apply to the profile's basis. To get the calories in two slices, find a portion that gives the weight of a slice, then scale a per-gram profile yourself.
Meal items
Smart search and image search return one item for each food, in the order the foods appear.
Meal item
textstring- The words of your query this item comes from, or a description of the food seen in the photo.
foodstring- The food name that was searched in the catalog.
servingobject- The amount you asked for: quantity and unit, both null when you gave none. A whole food is counted with the unit count. estimated is true only for a portion judged from a photo.
referencesarray of strings- English names used to find a misspelled, translated or regional food. Left out when there are none.
statusstring- matched, ambiguous, serving_unavailable or no_match.
Status
matched- One food was selected. The item includes all its fields.
ambiguous- Foods were found, but none fits confidently enough to choose for you. Ask your user to pick.
serving_unavailable- A food fits, but its records cannot express the unit you asked for. Ask your user for a weight.
no_match- Nothing in the catalog fits.
Errors
An error has a 4xx or 5xx status and the body below. Branch on code. The message is written for people and can change.
{ "error": { "code": "insufficient_credits", "message": "Your free queries or balance are used up. Add credits in Billing." } }
Codes
invalid_query, invalid_limit, invalid_barcode, invalid_request, invalid_image, invalid_image_url400- The request is malformed. The message names the field to fix.
unauthorized401- The API key is missing or not valid.
insufficient_credits402- Your free queries and balance are used up. Add credits in the console.
account_suspended403- The account is suspended.
not_found404- No food has this barcode, or the endpoint does not exist.
method_not_allowed405- The endpoint does not accept this HTTP method. The Allow header names the one it does.
request_too_large, image_too_large413- The body or image upload is over the size limit.
unsupported_image415- The image is not a JPEG or PNG.
not_food_query, not_food_image, too_many_foods, unverified_food_query422- The text or photo is not about food, names more than 12 foods, or produced foods and amounts that could not be verified against it.
rate_limit_exceeded, search_capacity_exceeded429- You sent more than 60 queries in a minute or submitted more than 1,000 foods in 24 hours, or search is busy. Wait for the number of seconds in the Retry-After header.
internal_error and codes ending in _failed, _unavailable or _timeout500 to 504- A temporary fault on our side. Retry after a short wait.
Once a query is accepted, its response carries an X-Request-ID header. The same ID appears in your console logs. Include it when you report a problem.
https://kcal.elbantli.com/mcp
MCP server
An assistant that supports the Model Context Protocol, such as Claude or Cursor, can query kcal itself. Add the server by its URL. The first time it connects, your browser opens so you can sign in to kcal and allow access.
claude mcp add --transport http kcal https://kcal.elbantli.com/mcp
In the Claude app, add a custom connector with the same URL. You can disconnect an app at any time in the console settings.
A client can also send an API key as the Bearer header, and then nobody has to sign in.
{ "mcpServers": { "kcal": { "url": "https://kcal.elbantli.com/mcp", "headers": { "Authorization": "Bearer YOUR_API_KEY" } } } }
The server has three tools: basic-search, smart-search and barcode. Each takes the parameters of the endpoint it is named after and returns that endpoint's JSON. Image search and food submission are not available as tools.
A tool call is one query. It is charged, counted toward your rate limit and shown in your logs like the HTTP request, under the app's name or the key's. A call that fails is not charged.