# kcal API reference > A nutrition API and food database with 5.2 million packaged and generic foods. It returns calories, macros and serving sizes as JSON, and searches by name, barcode, meal description or photo. The base URL is https://kcal.elbantli.com/v1. Every request sends an API key as a Bearer token: `Authorization: Bearer $KCAL_API_KEY`. Keys are created in the console at https://kcal.elbantli.com/console/settings. Every response is JSON. The first 500 successful queries are free. After that, each successful query, a response with a 2xx status, is paid from prepaid credit. Failed requests are not charged. An account can run 60 queries per minute across all its keys. Past that, requests return a 429 until the minute is over. The example responses are shortened. An … marks what is left out. ## Basic search `GET /v1/basic-search`, $0.003 per query. Finds foods by name, brand or barcode and returns the closest matches first. Query parameters: - `q` (string, required): A food or product name, or a barcode. 1 to 512 bytes of UTF-8. - `nb_results` (integer): 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. ```sh curl "https://kcal.elbantli.com/v1/basic-search?q=greek+yogurt&nb_results=1" \ -H "Authorization: Bearer $KCAL_API_KEY" ``` ```json { "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 }, …] }] } ``` ## Smart search `POST /v1/smart-search`, $0.040 per query. 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: - `q` (string, 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. ```sh 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"}' ``` ```json { "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" }] } ``` ## Image search `POST /v1/image-transcribe`, $0.055 per query. Identifies up to 12 foods in a photo, estimates each visible portion, and matches each food to the catalog. Multipart form fields: - `image` (file): A JPEG or PNG of at most 25 million pixels. The whole request can be at most 10 MiB. - `image_url` (string): 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. ```sh curl "https://kcal.elbantli.com/v1/image-transcribe" \ -H "Authorization: Bearer $KCAL_API_KEY" \ -F image=@lunch.jpg ``` ## Barcode lookup `GET /v1/barcode`, $0.002 per query. Returns the packaged food with exactly this barcode. Query parameters: - `barcode` (string, 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. ```sh curl "https://kcal.elbantli.com/v1/barcode?barcode=3017620422003" \ -H "Authorization: Bearer $KCAL_API_KEY" ``` ```json { "food": { "kind": "branded", "name": "Nutella", "brand": "Nutella, Ferrero", "barcode": "3017620422003", "relaxed": false, "relevance": 1, "nutrition": [ … ], "portions": [ … ] } } ``` ## Food submission `POST /v1/submit-food`, $0.002 per query. Stores a packaged food for a barcode the catalog does not have, so that the next lookup of that barcode returns it. JSON body: - `id` (string, required): A UUID that identifies this submission. The whole body can be at most 16 KiB. - `barcode` (string, required): The barcode as printed, 8 to 14 digits. Leading zeros count. - `name` (string, required): The product's name. 1 to 200 characters. - `brand` (string): The brand. At most 200 characters. - `basis` (object, 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. - `nutrients` (object, 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. - `serving` (object or null): The size of one serving: grams, millilitres or both, each a number above 0 or null. - `linkedId` (string): 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. ```sh 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 } }' ``` ```json { "id": "user:3017620422003" } ``` ## Food object Basic search, barcode lookup and matched meal items return foods in this shape. - `id` (string): The food's catalog ID. It stays the same between queries. - `kind` (string): branded for a packaged product. foundation, survey or legacy for a generic food. - `name` (string): The food's name. - `brand` (string): The brand, or an empty string for a generic food. - `barcode` (string): The barcode, or an empty string when the record has none. - `relaxed` (boolean): true when the food was found by leaving out one of your search terms. Treat it as a suggestion. - `relevance` (number): How well the food fits the query, from 0 to 1. It ranks results; it is not a probability. - `nutrition` (array): Every nutrition profile the source has, for example per 100 g and per serving. Empty when it has none. - `portions` (array): The food's serving sizes. Empty when the source has none. ### Nutrition profile - `key` (string): Identifies the profile within the food. - `preparation` (string): as_sold, prepared or unknown. - `basis` (object): 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. - `nutrients` (object): 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. - `portion` (object or null): The serving this profile was reported for, when it has one. - `estimated` (boolean): true when the values are the source's estimate, not a label or measurement. - `issues` (array of strings): Problems found in the source record, such as a nutrient with an unsupported unit. ### Portion - `key` (string): Identifies the portion within the food. - `preparation` (string): as_sold, prepared or unknown. - `label` (string): The serving as the source wrote it, such as 1 cup. - `quantity, unit` (number or null, string): The serving as a number and a unit, when the source gives them or the label could be parsed. - `grams, millilitres` (number 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. - `text` (string): The words of your query this item comes from, or a description of the food seen in the photo. - `food` (string): The food name that was searched in the catalog. - `serving` (object): 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. - `references` (array of strings): English names used to find a misspelled, translated or regional food. Left out when there are none. - `status` (string): 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. ```json { "error": { "code": "insufficient_credits", "message": "Your free queries or balance are used up. Add credits in Billing." } } ``` Each line lists error codes, then the HTTP status they arrive with. - `invalid_query, invalid_limit, invalid_barcode, invalid_request, invalid_image, invalid_image_url` (400): The request is malformed. The message names the field to fix. - `unauthorized` (401): The API key is missing or not valid. - `insufficient_credits` (402): Your free queries and balance are used up. Add credits in the console. - `account_suspended` (403): The account is suspended. - `not_found` (404): No food has this barcode, or the endpoint does not exist. - `method_not_allowed` (405): The endpoint does not accept this HTTP method. The Allow header names the one it does. - `request_too_large, image_too_large` (413): The body or image upload is over the size limit. - `unsupported_image` (415): The image is not a JPEG or PNG. - `not_food_query, not_food_image, too_many_foods, unverified_food_query` (422): 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_exceeded` (429): 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 _timeout` (500 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 the console logs. ## MCP server An assistant that supports the Model Context Protocol can query kcal itself at https://kcal.elbantli.com/mcp. The first time it connects, a browser opens to sign in to kcal and allow access. ```sh claude mcp add --transport http kcal https://kcal.elbantli.com/mcp ``` A client can also send an API key as the Bearer header, and then nobody has to sign in. ```json { "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. A tool call is one query: it is charged, counted toward the rate limit and logged like the HTTP request.