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.
| Anfrage | Verwendet für | Wenn deinem Server dies fehlt |
|---|---|---|
POST /api/v1/foods/search | Lebensmittelsuche sowie Vitamine und Mineralstoffe eines neuen Eintrags | Lebensmittelsuche 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/nutrients | Referenzaufnahmemengen, damit die App Lücken anzeigen kann | Die 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ährstoffansicht | Die 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_KEYgesetzt ist, trägt jede Anfrageauthorization: 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.
nullist nicht0.nullbedeutet: „Diese Quelle hat keine Angabe“.0bedeutet: „Wurde gemessen und beträgt null“. Die App zählt nur gemessene Werte, sende daher für einen unbekannten Wert niemals0.- 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.
Suche
POST /api/v1/foods/search
Request-Body:
{ "q": "hähnchenbrust", "locale": "de", "limit": 10 }qist der Lebensmittelname, den die Person eingegeben oder den die KI erkannt hat.localeist einer der Werteen,de,fr,it,es,tr. Gib Titel in dieser Sprache zurück, sofern du sie vorliegen hast.limitist heute 10.
Antwort:
{
"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 }
}
]
}| Feld | Typ | Bedeutung |
|---|---|---|
slug | String, erforderlich | Eine stabile ID für das Lebensmittel. Die App speichert sie und fragt sie später in der Detailanfrage erneut ab. |
locale | String, erforderlich | Die Sprache von title. |
title | String, erforderlich | Der anzuzeigende Name. |
canonicalName | String, erforderlich | Ein englischer Name, der als stabile Bezeichnung dient. |
url | String oder null | Eine Seite für das Lebensmittel. null, wenn keine existiert. Fehlt die Angabe, gilt sie als null. |
imageUrl | String oder null, erforderlich | Ein Bild oder null. |
macrosPer100g | Objekt, erforderlich | kcal, protein, fat, carbs, fiber, sugars, polyols. Jeder Wert ist eine Zahl oder null. Alle sieben Schlüssel müssen vorhanden sein. |
netCarbsPer100g | Zahl oder null, erforderlich | Kohlenhydrate ohne Ballaststoffe. Siehe Kohlenhydrate. |
attribution | String oder null | Der Lizenzhinweis für importierte Daten. Die App zeigt ihn an. |
score | Zahl, erforderlich | Relevanz, 0 bis 1. Siehe Rangfolge. |
origin | String oder null | Herkunft des Lebensmittels. Bestimmt, wie die App carbs liest. Siehe Kohlenhydrate. |
portionSize | Zahl oder null | Eine typische Portionsgröße in Gramm. |
vitamins | Objekt oder null | Siehe Vitamine und Mineralstoffe. Weglassen, wenn das Lebensmittel keinerlei Vitamindaten enthält. |
minerals | Objekt oder null | Dasselbe 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:
origin | Basis | Was macrosPer100g.carbs bedeutet |
|---|---|---|
bls, curated | verfügbar (EU) | Kohlenhydrate ohne Ballaststoffe. Ballaststoffe bilden eine eigene Zeile. |
fdc, user, proposal | gesamt (US) | Kohlenhydrate mit Ballaststoffe. Netto-Kohlenhydrate entsprechen Kohlenhydraten minus Ballaststoffen. |
| nicht vorhanden oder ein beliebiger anderer Wert | unbekannt | Die 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
scorevon mindestens 0,4 muss einem echten Texttreffer entsprechen (exakt, Präfix, Wort oder Teilstring). Unterhalb von 0,45 verwirft die App ihn. - Ein
scoreunterhalb 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:
| Block | Schlüssel | Einheit |
|---|---|---|
vitamins | betaCarotene, vitaminA, vitaminD, vitaminB9, vitaminB12 | µg |
vitamins | vitaminC, vitaminE, vitaminB1, vitaminB2, vitaminB6 | mg |
minerals | nacl, potassium, sodium, calcium, magnesium, zinc, phosphorus, iron | mg |
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:
- Block fehlt (oder
null): Dieses Lebensmittel enthält überhaupt keine Vitamin- oder Mineralstoffdaten. - Schlüssel ist
null: Der Block existiert und dieser einzelne Wert ist unbekannt. - Schlüssel ist eine Zahl: Ein gemessener Wert.
0sind 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,vitaminsundminerals. 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.
{
"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
}
]
}slugmuss einer der unten aufgeführten Slugs sein. Andere ignoriert die App.kindisttarget("hast du genug zu dir genommen?") oderceiling("bist du darunter geblieben?"). Derzeit ist nur Natrium einceiling. Ein unbekannter oder fehlender Wert wird alstargetgelesen.rdaDe,rdaEuundrdaUssind drei separat veröffentlichte Dokumente: DGE, EFSA und die USA (NASEM). Sende die, die du hast, undnullfü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-70undover_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. pregnancyundlactationsind 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üssel | Slug |
|---|---|
vitaminA | vitamin-a-retinol |
vitaminC | vitamin-c |
vitaminD | vitamin-d |
vitaminE | vitamin-e-tocopherole |
vitaminB1 | vitamin-b-1-thiamine |
vitaminB2 | vitamin-b-2-riboflavin |
vitaminB6 | vitamin-b-6-pyridoxine |
vitaminB9 | folic-acid-folate |
vitaminB12 | vitamin-b-12-cobalamin |
potassium | potassium |
sodium | sodium |
calcium | calcium |
magnesium | magnesium |
zinc | zinc |
phosphorus | phosphorus |
iron | iron |
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
{
"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
| Status | Bedeutung | Verhalten der App |
|---|---|---|
| 401 | Der Schlüssel ist falsch oder fehlt | Protokolliert invalid_key, zeigt einen Hinweis an, arbeitet weiter mit KI-Werten |
| 429 | Kontingent oder Rate überschritten | Protokolliert rate_limited, oder allowance_exhausted, wenn der Body dies angibt |
| alles andere | Fehlschlag | Wird 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.
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) sowieapp/services/food-db/micronutrients-wire.ts(die interne Backfill-Route der App, die nicht Teil dieses Vertrags ist).