Zum Inhalt springen

Die App

API-Vertrag der Lebensmitteldatenbank (experimentell)

Experimentell. Die Anfragen und Antworten, die eine Lebensmitteldatenbank liefern muss, damit FOOD_DB_API_URL auf einen anderen Server als lowcarbcheck.org verweisen kann

Diese Seite wurde maschinell aus der englischen Dokumentation übersetzt.

Experimentell. Diese Seite beschreibt, was der App-Server heute von einer Lebensmitteldatenbank anfordert. Es ist keine stabile API. Ein Minor-Release kann sie ändern, was im Changelog vermerkt wird. Der einzige Server, der sie implementiert, ist lowcarbcheck.org. Es gibt noch keinen Open-Source-Server und keinen Konformitätstest. Wenn du einen baust, melde dich in einem Issue, damit der Vertrag mit einem zweiten Nutzer wachsen kann.

openplate schlägt Lebensmittel nach Namen über den eigenen Server nach. Standardmäßig fragt dieser Server lowcarbcheck.org ab. FOOD_DB_API_URL verweist ihn auf einen anderen Server, der dieselben Anfragen beantwortet. configuration.md erklärt den Schlüssel und das Kontingent. Diese Seite erklärt die Anfragen und Antworten.

Was der Vertrag enthält

Der App-Server stellt vier Arten von Anfragen. Nur die erste ist erforderlich.

AnfrageVerwendet fürWenn deinem Server dies fehlt
POST /api/v1/foods/searchLebensmittelsuche sowie Vitamine und Mineralstoffe eines neuen EintragsLebensmittelsuche ist leer. Scans behalten die eigenen Zahlen der KI.
GET /api/v1/foods/{slug}Vitamine für alte Tagebucheinträge (das Nachpflegen)Alte Einträge bleiben ohne Vitamine.
GET /api/v1/nutrientsReferenzaufnahmemengen, damit die App Lücken anzeigen kannDie App zeigt, was ein Tag enthält, aber keine Lücken.
GET /api/v1/nutrients/{slug}/foods"Lebensmittel reich an diesem Nährstoff" in der NährstoffansichtDie Liste ist leer.

Der App-Server behandelt jeden Fehler als leere Antwort. Ein fehlender Endpunkt, ein Timeout, ein Nicht-2xx-Status oder ein nicht passender Body stoppt niemals einen Scan oder eine Tagebuchseite. Das nennt sich Fail-Open.

Regeln für jede Anfrage

  • Die Basisadresse ist FOOD_DB_API_URL. Die Pfade oben werden daran angehängt.
  • Nur JSON. Anfragen senden accept: application/json. Antworten sind UTF-8-JSON.
  • Der Schlüssel. Wenn FOOD_DB_API_KEY gesetzt ist, trägt jede Anfrage authorization: Bearer <key>. Dein Server bestimmt, was ein Schlüssel bedeutet. Ist kein Schlüssel gesetzt, fehlt der Header.
  • Das Timeout beträgt 4 Sekunden pro Anfrage. Eine langsamere Antwort gilt als Fehler.
  • Unbekannte Felder werden ignoriert. Du kannst Felder frei hinzufügen. Ein fehlendes optionales Feld gilt als „kein Wert“.
  • Pro 100 g. Alle Nährwertangaben beziehen sich auf 100 g des Lebensmittels. Energie wird in kcal angegeben, Makronährstoffe in Gramm.
  • null ist nicht 0. null bedeutet: „Diese Quelle hat keine Angabe“. 0 bedeutet: „Wurde gemessen und beträgt null“. Die App zählt nur gemessene Werte, sende daher für einen unbekannten Wert niemals 0.
  • Datenschutz. Die Suchanfrage überträgt den Lebensmittelnamen im POST-Body, nicht in der Adresse, damit kein Name in einem Zugriffsprotokoll landet. Behalte diese Eigenschaft bei: Protokolliere keine Request-Bodys. Die App sendet weder Account- noch Tagebuch- oder Körperdaten. Siehe architecture.md.

POST /api/v1/foods/search

Request-Body:

json
{ "q": "hähnchenbrust", "locale": "de", "limit": 10 }
  • q ist der Lebensmittelname, den die Person eingegeben oder den die KI erkannt hat.
  • locale ist einer der Werte en, de, fr, it, es, tr. Gib Titel in dieser Sprache zurück, sofern du sie vorliegen hast.
  • limit ist heute 10.

Antwort:

json
{
  "results": [
    {
      "slug": "haehnchenbrust-roh",
      "locale": "de",
      "title": "Hähnchenbrust, roh",
      "canonicalName": "Chicken breast, raw",
      "url": null,
      "imageUrl": null,
      "macrosPer100g": {
        "kcal": 106, "protein": 23.1, "fat": 1.2,
        "carbs": 0, "fiber": 0, "sugars": 0, "polyols": 0
      },
      "netCarbsPer100g": 0,
      "attribution": "BLS 4.0, CC BY 4.0",
      "score": 0.92,
      "origin": "bls",
      "portionSize": null,
      "vitamins": { "vitaminA": 9, "vitaminC": 0, "vitaminB12": 0.3 },
      "minerals": { "sodium": 60, "potassium": 370 }
    }
  ]
}
FeldTypBedeutung
slugString, erforderlichEine stabile ID für das Lebensmittel. Die App speichert sie und fragt sie später in der Detailanfrage erneut ab.
localeString, erforderlichDie Sprache von title.
titleString, erforderlichDer anzuzeigende Name.
canonicalNameString, erforderlichEin englischer Name, der als stabile Bezeichnung dient.
urlString oder nullEine Seite für das Lebensmittel. null, wenn keine existiert. Fehlt die Angabe, gilt sie als null.
imageUrlString oder null, erforderlichEin Bild oder null.
macrosPer100gObjekt, erforderlichkcal, protein, fat, carbs, fiber, sugars, polyols. Jeder Wert ist eine Zahl oder null. Alle sieben Schlüssel müssen vorhanden sein.
netCarbsPer100gZahl oder null, erforderlichKohlenhydrate ohne Ballaststoffe. Siehe Kohlenhydrate.
attributionString oder nullDer Lizenzhinweis für importierte Daten. Die App zeigt ihn an.
scoreZahl, erforderlichRelevanz, 0 bis 1. Siehe Rangfolge.
originString oder nullHerkunft des Lebensmittels. Bestimmt, wie die App carbs liest. Siehe Kohlenhydrate.
portionSizeZahl oder nullEine typische Portionsgröße in Gramm.
vitaminsObjekt oder nullSiehe Vitamine und Mineralstoffe. Weglassen, wenn das Lebensmittel keinerlei Vitamindaten enthält.
mineralsObjekt oder nullDasselbe für Mineralstoffe.

Kohlenhydrate

Zwei Quellen geben Kohlenhydrate auf zwei Arten an, und die App muss wissen, welche Art ein Lebensmittel verwendet. Das Feld origin gibt dies an:

originBasisWas macrosPer100g.carbs bedeutet
bls, curatedverfügbar (EU)Kohlenhydrate ohne Ballaststoffe. Ballaststoffe bilden eine eigene Zeile.
fdc, user, proposalgesamt (US)Kohlenhydrate mit Ballaststoffe. Netto-Kohlenhydrate entsprechen Kohlenhydraten minus Ballaststoffen.
nicht vorhanden oder ein beliebiger anderer WertunbekanntDie App behandelt dies als Gesamtwert, und die Person kann den Wert manuell anpassen.

Wenn deine Daten im EU-Format vorliegen, sende origin: "curated". Wenn sie im US-Format vorliegen, sende origin: "fdc". Ein falscher Wert verändert die Kohlenhydrat- und Kalorienwerte jedes betroffenen Lebensmittels, prüfe ihn also anhand eines Lebensmittels, das du kennst.

netCarbsPer100g ist deine eigene Angabe für Netto-Kohlenhydrate pro 100 g, und die App vertraut ihr mehr als ihrer eigenen Berechnung. null bedeutet "für dieses Lebensmittel unbekannt". Die App berechnet Netto-Kohlenhydrate dann aus den Makronährstoffen und der oben genannten Basis. Sende niemals 0 für einen unbekannten Wert.

Rangfolge

Sortiere results nach Relevanz, beste Treffer zuerst. Die App behält deine Sortierung bei. Sie verwirft schwache Treffer anhand von score:

  • Ein score von mindestens 0,4 muss einem echten Texttreffer entsprechen (exakt, Präfix, Wort oder Teilstring). Unterhalb von 0,45 verwirft die App ihn.
  • Ein score unterhalb von 0,4 muss einem reinen Tippfehlertreffer entsprechen. Unterhalb von 0,3 verwirft die App ihn.
  • Ein Tippfehlertreffer darf niemals eine Bewertung von 0,4 oder höher erreichen, da er sonst vor einem echten Treffer rangiert.

Vitamine und Mineralstoffe

Jedes Lebensmittelobjekt enthält zwei optionale Blöcke, vitamins und minerals. Ein Block enthält die folgenden Schlüssel und keine weiteren, die die App liest:

BlockSchlüsselEinheit
vitaminsbetaCarotene, vitaminA, vitaminD, vitaminB9, vitaminB12µg
vitaminsvitaminC, vitaminE, vitaminB1, vitaminB2, vitaminB6mg
mineralsnacl, potassium, sodium, calcium, magnesium, zinc, phosphorus, ironmg

Jeder Wert ist eine Zahl oder null. Ein weggelassener Schlüssel wird als null gelesen. Der Block selbst hat drei Zustände, und sie bedeuten nicht dasselbe:

  1. Block fehlt (oder null): Dieses Lebensmittel enthält überhaupt keine Vitamin- oder Mineralstoffdaten.
  2. Schlüssel ist null: Der Block existiert und dieser einzelne Wert ist unbekannt.
  3. Schlüssel ist eine Zahl: Ein gemessener Wert. 0 sind echte Daten.

Die App zählt einen Nährstoff für einen Tag nur dann als abgedeckt, wenn ein ausreichend großer Gewichtsanteil der Lebensmittel einen gemessenen Wert aufweist (aktuell 60 Prozent). Falsche Nullen erwecken den Eindruck, als hätte die Person gar nichts von einem Vitamin zu sich genommen, sende also null, wenn du dir nicht sicher bist.

Vitamin A wird in Retinol-Aktivitätsäquivalenten (RAE) angegeben, was die Carotinoide bereits einschließt. betaCarotene dient dem Kontext, ist kein Zusatz. Wenn deine Quelle Retinol-Äquivalente (RE) oder Internationale Einheiten angibt, rechne sie vor dem Senden um, sonst stimmen die Summen nicht.

Lebensmitteldetails

GET /api/v1/foods/{slug}?locale=de

Die App fragt nach den Vitaminen für ein Lebensmittel, das jemand protokolliert hat, bevor die Vitaminfunktion existierte. Sie sendet den slug aus einem alten Suchergebnis.

  • Gefunden: Status 200 und ein Lebensmittelobjekt mit slug, vitamins und minerals. Dieselben Blöcke wie in der Suche. Andere Felder werden ignoriert.
  • In diesem Gebietsschema nicht gefunden: Status 404 mit exakt diesem Body: { "error": "not_found" }. Die App versucht es dann mit den anderen Gebietsschemata (de, en, fr, it, es, mit der Sprache der Person zuerst), sodass ein Lebensmittel, das nur in einem Gebietsschema bekannt ist, dennoch gefunden wird. Jeder andere 404-Body gilt als Fehler, nicht als "nicht gefunden".
  • Gefunden und keine Blöcke: 200, wobei beide Blöcke fehlen. Die App speichert "geprüft, keine Daten" und fragt nicht noch einmal an.

Jede Abfrage kann auf deiner Seite ein Guthaben kosten, bis zu fünf für einen Slug, den kein Gebietsschema kennt. Die App fragt einmal pro eindeutigem Lebensmittel pro Gerät an.

Referenzmengen

GET /api/v1/nutrients

Die folgenden Werte dienen nur der Veranschaulichung der Struktur. Es sind keine echten Referenzmengen.

json
{
  "nutrients": [
    {
      "slug": "vitamin-c",
      "unit": "mg",
      "kind": "target",
      "rdaDe": {
        "source": "DGE 2024",
        "male":   { "14-18": 105, "19-30": 110, "31-50": 110, "51-70": 110, "over_70": 110 },
        "female": { "14-18": 90,  "19-30": 95,  "31-50": 95,  "51-70": 95,  "over_70": 95 },
        "pregnancy": 105,
        "lactation": 125
      },
      "rdaEu": null,
      "rdaUs": null
    }
  ]
}
  • slug muss einer der unten aufgeführten Slugs sein. Andere ignoriert die App.
  • kind ist target ("hast du genug zu dir genommen?") oder ceiling ("bist du darunter geblieben?"). Derzeit ist nur Natrium ein ceiling. Ein unbekannter oder fehlender Wert wird als target gelesen.
  • rdaDe, rdaEu und rdaUs sind drei separat veröffentlichte Dokumente: DGE, EFSA und die USA (NASEM). Sende die, die du hast, und null für den Rest. Die App zeigt jeweils nur eine Basis an, ausgewählt über die Instanzeinstellung, und vermischt nie zwei miteinander.
  • Die Altersgruppen sind exakt 14-18, 19-30, 31-50, 51-70 und over_70. Für beide Geschlechter werden alle fünf benötigt. Es gibt keine jüngere Altersgruppe. Für Personen unter 14 Jahren zeigt die App keinen Referenzwert an.
  • pregnancy und lactation sind optionale Zahlen. Die App verwendet sie nur lokal. Es werden zu keinem Zeitpunkt Daten über eine Person an dich gesendet.

Nährstoff-Slugs:

App-SchlüsselSlug
vitaminAvitamin-a-retinol
vitaminCvitamin-c
vitaminDvitamin-d
vitaminEvitamin-e-tocopherole
vitaminB1vitamin-b-1-thiamine
vitaminB2vitamin-b-2-riboflavin
vitaminB6vitamin-b-6-pyridoxine
vitaminB9folic-acid-folate
vitaminB12vitamin-b-12-cobalamin
potassiumpotassium
sodiumsodium
calciumcalcium
magnesiummagnesium
zinczinc
phosphorusphosphorus
ironiron

betaCarotene hat keinen eigenen Referenzwert, da es Teil von Vitamin A ist.

Nährstoffreiche Lebensmittel

GET /api/v1/nutrients/{slug}/foods?locale=en&limit=8

json
{
  "nutrient": "vitamin-c",
  "unit": "mg",
  "foods": [
    { "slug": "acerola-raw", "title": "Acerola, raw", "url": null, "imageUrl": null,
      "netCarbsPer100g": 7.7, "attribution": null, "value": 1677.6 }
  ]
}

Sortiere foods mit den reichhaltigsten zuerst. value ist die Menge pro 100 g und ist nie null. Lass Lebensmittel ohne Angabe weg, statt sie als 0 zu senden.

Fehler

StatusBedeutungVerhalten der App
401Der Schlüssel ist falsch oder fehltProtokolliert invalid_key, zeigt einen Hinweis an, arbeitet weiter mit KI-Werten
429Kontingent oder Rate überschrittenProtokolliert rate_limited, oder allowance_exhausted, wenn der Body dies angibt
alles andereFehlschlagWird ohne Hinweis wie „keine Antwort“ behandelt

Für 401 und 429 kannst du einen Body im Format { "error": { "code": "allowance_exhausted" } } senden. Die erkannten Codes sind invalid_key, rate_limited und allowance_exhausted. Bei einem unbekannten Code greift der Grund, den der Status impliziert. Die App speichert eine Verweigerung nie im Cache.

Teste deinen Server

Führe diese Befehle gegen deinen Server aus. Jeder sollte JSON zurückgeben, das den obigen Abschnitten entspricht.

bash
BASE=https://food.example.org
curl -s -X POST "$BASE/api/v1/foods/search" -H 'content-type: application/json' \
  -d '{"q":"apple","locale":"en","limit":3}'
curl -s "$BASE/api/v1/foods/apple-raw?locale=en"
curl -s "$BASE/api/v1/foods/does-not-exist?locale=en" -w '\n%{http_code}\n'   # 404, {"error":"not_found"}
curl -s "$BASE/api/v1/nutrients"

Setze dann FOOD_DB_API_URL auf dem App-Server, starte ihn neu und suche in der App nach einem Lebensmittel. Ein protokolliertes Lebensmittel mit einem vitamins-Block zählt dann in der Karte für Vitamine und Mineralstoffe seines Tagebuch-Tages. Falls nicht, lies das App-Server-Log: Eine Verweigerung wird als Warnung mit den ersten 16 Zeichen des Schlüssels protokolliert, nie mit dem Schlüssel selbst.

Was nicht abgedeckt ist

  • Es wird kein Server bereitgestellt. Du baust einen eigenen oder nutzt lowcarbcheck.org.
  • Lebensmittelvorschläge (POST /api/v1/foods/proposals, FOOD_DB_BACKFILL) sind eine separate, LowCarbCheck-spezifische Funktion und nicht Teil dieses Vertrags.
  • Noch keine Versionierung. Die Pfade enthalten /v1, die Struktur kann sich jedoch noch ändern, solange diese Seite als „experimentell“ gekennzeichnet ist.
  • Die Übertragungsschemata liegen im Code. Die maßgebliche Quelle ist heute app/services/food-resolution/schema.ts (Suche und Details), app/lib/nutrient-reference.ts (Referenzmengen und nährstoffreiche Lebensmittel) sowie app/services/food-db/micronutrients-wire.ts (die interne Backfill-Route der App, die nicht Teil dieses Vertrags ist).

Diese Seite auf GitHub bearbeiten