JSON API v1

Food verdicts for your app

Everything the website shows is available as JSON. Requests need the server's API key — send it as an X-API-Key header (or Authorization: Bearer <key>). Responses are never cached by intermediaries (Cache-Control: no-store) and allow CORS from any origin.

Endpoints

EndpointWhat it does
GET /api/v1Discovery document: enabled sources, endpoints, languages and classifier version.
GET /api/v1/search?q=…Name/category search (Dabas first, then Open Food Facts). If q is a barcode the product is looked up directly and returned inline.
GET /api/v1/product/{gtin}Product with ingredient list, nutrition facts, the overall healthy verdict and the FODMAP, keto, GI and processed verdicts. Accepts EAN-8, UPC-A, EAN-13 and GTIN-14 (leading zeros are normalised).

Examples

# product by barcode
curl -H "X-API-Key: $API_KEY" "https://maxing.fitness/api/v1/product/3017624010701"

# free-text search, texts in English
curl -G -H "X-API-Key: $API_KEY" --data-urlencode "q=oats" \
  "https://maxing.fitness/api/v1/search?lang=en"

Languages

Summaries and reasons are in English by default. Add lang with one of these codes to get them in another EU language: bg, es, cs, da, de, et, el, en, fr, ga, hr, it, lv, lt, hu, mt, nl, pl, pt, ro, sk, sl, fi, sv. Ratings, scores, rule ids and missing keys are the same in every language.

Response envelope

{
  "ok": true,
  "data": {
    "product":  { "gtin": "3017624010701", "name": "Nutella", "brand": "Ferrero", "ingredientsText": "…",
                  "nutrition": { "basis": "100g", "carbs": 57.5, "sugars": 56.3, … },
                  "novaGroup": 4, "images": { "front": "…" }, "sources": [ { "name": "off", … } ] },
    "verdicts": {
      "healthy":   { "rating": "red",    "summary": "Nutri-Score E, ultra-processed, high in sugar and saturated fat", … },
      "fodmap":    { "rating": "yellow", "summary": "…", "reasons": ["…"], "matches": [ … ] },
      "keto":      { "rating": "red",    … },
      "glycemic":  { "rating": "yellow", … },
      "processed": { "rating": "red",    … }
    },
    "ingredients": [ { "text": "socker", "depth": 0, "section": "main" }, … ],
    "traces": ["nötter"],
    "labels": [],
    "cache": { "status": "hit", "fetchedAt": "2026-08-26T18:00:00.000Z", "partial": false }
  },
  "meta": { "classifierVersion": 2, "cache": "hit", "sources": ["off"], "attribution": { … } }
}

Ratings

Every verdict has a rating of green, yellow, red or unknown, a one-line summary, human-readable reasons, and matches pointing at the ingredients (by index into ingredients) that triggered it. missing lists data that would improve the verdict. healthy is the overall answer: green for products with the Keyhole (Nyckelhålet), otherwise the Nutri-Score (from Open Food Facts, or estimated with the 2023 algorithm, in which case score holds its points), one step down for ultra-processed food. The heuristics are documented in the README; bump-worthy changes increment meta.classifierVersion (also sent as the X-Classifier-Version header).

Errors

{ "ok": false, "error": { "code": "unauthorized", "message": "…" } }

400 invalid_gtin · 401 unauthorized · 404 product_not_found / not_found
503 api_not_configured · 503 upstream_unavailable / rate_limited (with Retry-After)

Barcodes from a phone

Native apps can scan with any barcode library and call GET /api/v1/product/{gtin} with the raw digits: EAN-8, UPC-A (12 digits), EAN-13 and zero-padded GTIN-14 are all normalised to the same product. UPC-E must be expanded to UPC-A first. The website's own scanner does exactly this in the browser.

Caching & rate limits

Products are cached for 30 days per GTIN and refreshed from the source databases afterwards; a stale copy is served if the databases are down. Open Food Facts allows ~15 product reads and ~10 searches per minute per IP, so bursts of uncached lookups may answer 503 rate_limited — honour Retry-After. Data: Open Food Facts (ODbL) and Dabas.