TempoLife › Features › Developer portal › docs
Endpoint reference
The complete reference for the two public TempoLife endpoints: every query parameter, every response field, every status code, the CORS and caching headers, and the errors you should expect to handle.
Overview · Endpoint reference · Your API keys · Your usage
Keys are not enforced yet. /food-api.php and /api/answer are open, unauthenticated and CORS-open right now, and no code in TempoLife reads an API key when answering them. A key you create here reserves your identity and the higher ceiling you will get when enforcement ships; sending it today changes nothing about the response you receive or the limit you are under.
Base URL and transport
Everything is served from https://tempolife.app over TLS. There is no versioned path prefix; the contract is versioned in the OpenAPI file (currently 1.0.0). Both endpoints send Access-Control-Allow-Origin: * and Access-Control-Allow-Methods: GET, OPTIONS, so browser calls need no proxy of your own.
GET /food-api.php
Reference nutrition per 100 g of edible portion. Called with no parameters it renders human-readable HTML documentation instead of JSON — so always send either food or search.
| Parameter | Type | Required | Meaning |
|---|---|---|---|
food | string | one of the two | Canonical slug. Duplicate slugs are followed one hop to their canonical row before lookup. |
search | string | one of the two | Case-insensitive substring match on English name or slug. Max 20 results. Wins over food if both are sent. |
lang | et | fi | ru | no | Adds name_<lang> when a distinct translation exists. Any other value is ignored, not rejected. |
Response fields — single food
| Field | Type | Notes |
|---|---|---|
dataset_license | string | Always "mixed". It is a warning, not a licence. |
licensing | object | Per-class summary: CC-BY-4.0 schema and localisation, us_public_domain USDA values, null for curated values. |
attribution | string | The URL to credit. |
food.slug | string | Canonical slug, stable across releases. |
food.name | string | English display name. |
food.category | string | Defaults to "Other". Category labels are not English-normalised — do not present them raw to users. |
food.source | string | usda_sr_legacy or curated_unverified. |
food.source_ref | string | null | Row-level external reference where one was retained. Null on curated rows, and null on many USDA-family rows too. |
food.values_license | string | null | The only field that tells you what you may do with the numbers. |
food.per_100g | object | kcal, protein_g, carbs_g, fat_g, fiber_g, sugar_g and further keys. sugar_g is null on every curated row — unknown, not zero. |
food.url | string | Canonical human page for the food. |
food.name_et / _fi / _ru | string | Present only when lang was sent and a distinct translation exists. |
Status codes
| Code | Body | When |
|---|---|---|
| 200 | food or results envelope | Normal. |
| 404 | {"error":"food_not_found","slug":…,"hint":…} | Unknown slug. The hint points at the search URL — follow it rather than guessing. |
| 503 | {"error":"dataset_unavailable"} | The dataset file is missing, unreadable, outside 2 bytes … 16 MB, or fails one of the row assertions. Retry later; this is never your fault. |
Search never 404s. A query that matches nothing returns 200 with "count": 0 and an empty results array, which is the case most clients forget to handle.
GET /api/answer
One question in, one grounded sentence out, with the numbers and the source beside it. Reachable at both /api/answer and /api/answer.php; the extensionless form is the one robots.txt allows and the one the OpenAPI file publishes.
| Parameter | Type | Required | Meaning |
|---|---|---|---|
q | string, 1–160 chars | yes | A natural-language question. Longer strings and control characters are rejected as not-found rather than truncated. |
What it understands
Metric keywords map to eight fields: calories / calorie / kcal / energy, protein, carbs / carbohydrate, fat, fiber / fibre, sugar / sugars, saturated fat / sat fat and salt / sodium. No metric keyword means energy. A query matching A vs B, A versus B or A and B is answered as a comparison and returns a foods array plus source_urls instead of a single food.
| Code | Body | When |
|---|---|---|
| 200 | answer object | A food matched and the requested metric exists on it. |
| 204 | empty | Preflight OPTIONS. |
| 400 | {"error":"missing_query","example":…} | q absent or empty after trimming. |
| 404 | {"error":"answer_not_found","query":…,"hint":…} | No food matched, or the matched food has no stored value for that metric — a curated row asked for sugar, for instance. |
| 405 | {"error":"method_not_allowed"} | Anything but GET or OPTIONS. An Allow header comes with it. |
| 429 | {"error":"rate_limited","retry_after":…} | Over 60 requests this hour from your IP. |
Note the shape of a 404 here: it is a normal outcome, not a fault. Roughly one in ten plausible-looking food phrasings will not resolve, and a client that treats 404 as an exception will be noisy for no reason. Fall back to /food-api.php?search= and let the person pick.
Handling the limit properly
async function ask(q) {
const res = await fetch('https://tempolife.app/api/answer?q=' + encodeURIComponent(q));
if (res.status === 429) {
const wait = Number(res.headers.get('Retry-After') || 60);
throw Object.assign(new Error('rate limited'), { retryAfterSeconds: wait });
}
if (res.status === 404) return null; // normal: no grounded answer
if (!res.ok) throw new Error('HTTP ' + res.status);
return res.json();
}
// Budget: 60 calls per hour per IP. Cache by normalised question text —
// the same question always returns the same stored number.Caching and conditional requests
/food-api.php sends Cache-Control: public, max-age=86400 and X-Data-License: mixed; inspect source and values_license per row. /api/answer sends public, max-age=3600. Neither endpoint emits an ETag or Last-Modified, so conditional requests will not save you a round trip — cache on time, not on validators.
The bulk download
/datasets/foods.json is the same table as one file, around 1.9 MB. Its top level is { "count": 5 038, "foods": [ … ] } and each element carries the same provenance fields as an API row. If you need more than a few hundred foods, take the file: it is one request instead of thousands, and it is the access pattern this project actually wants you to use.
Other public JSON on the site
One endpoint outside the OpenAPI file is worth knowing about because a public embed already depends on it: /counter.php?format=json returns the delayed, contribution-capped aggregate community counter that /badge.js renders. It is CORS-open and cached for an hour, it is marked X-Robots-Tag: noindex, and it exposes aggregates only — never a person, a meal or an account. It is undocumented in the OpenAPI contract, so treat its shape as less stable than the two endpoints above.
Terms in one paragraph
Free, unauthenticated, no SLA. Values are per 100 g reference figures — they describe a food, not the plate in front of someone, and they are not medical or dietetic advice. Cache aggressively, attribute TempoLife where you display the data, keep the per-row provenance you were given, and do not present curated-row numbers as though they carried a licence. If the service goes down mid-request, fail soft: nothing here is a system of record.
Licensing, and why it is not one licence
The dataset behind /food-api.php has mixed provenance, and the API refuses to hide that. Every row carries its own source, a nullable source_ref and a nullable values_license, and the response envelope repeats the summary in licensing. You are expected to read those fields per row rather than assume a blanket grant.
| Row class | Rows | values_license | What you may assume |
|---|---|---|---|
usda_sr_legacy | 4 612 | us_public_domain | Nutrition values originate in the USDA SR Legacy source family. US Government works, public domain in the United States. |
curated_unverified | 426 | null | No retained source reference and no asserted value-reuse right. Their sugar_g is null, not zero. Filter these out when you need clean provenance. |
Separately, TempoLife offers its metadata schema and its own localisation under CC BY 4.0 with attribution to https://tempolife.app/. That grant covers the shape of the data and the translated names TempoLife wrote. It does not relicense third-party nutrition values, and it cannot create a reuse right for the curated rows that never had one.
Practical rule: keep source, source_ref and values_license next to any value you store, and attribute TempoLife where you display it. If your product needs a single clean licence, request only USDA-family rows and drop the rest.
Source: TempoLife food nutrition API — provenance section · checked 2026-09-02
Frequently asked questions
Which HTTP methods are allowed?
/api/answer answers GET and OPTIONS and returns 405 with an Allow header for anything else. /food-api.php reads only the query string, so any method reaches the same code path — treat it as GET-only.
Why did I get a 503?
food-api.php validates its dataset on every request — row count, per-row provenance fields and the split between USDA-family and curated rows. If any assertion fails it returns 503 dataset_unavailable rather than serve a half-trusted table.
Is the answer endpoint an LLM?
No. It matches your phrasing to a metric and a food name, then formats a stored number. It has no generative step, which is exactly why it can hand you a canonical source URL for every answer.
Back to the developer overview · OpenAPI 3.1 file · The endpoint's own docs page
Want the numbers in a diary, not a terminal?
The same food table powers the TempoLife app: photograph a meal, get the macros, and watch the weekly trend instead of a single row.