Nutrition infrastructure for Portugal
NauTritiva gives applications one canonical API for Portuguese foods, European products, complete nutrient panels, barcode lookup, meal calculation, recipes, classification, provenance, and dated supermarket-price observations.
PT records rank ahead of equally relevant foreign records. Global data remains available as fallback.
Every record carries source, confidence, attribution, timestamps, and nulls for genuinely missing values.
Searches and product views queue background discovery, nutrition refresh, and price synchronization.
null, never guessed. Allergens are only returned when explicitly declared by a source.Quick start
1. Create a developer key
curl -X POST https://your-domain.com/api/dev/signup \
-H "content-type: application/json" \
-d '{"name":"My App","email":"developer@example.com"}'The plaintext key is returned once. Store it securely; NauTritiva stores only its SHA-256 hash.
2. Authenticate every /v1 request
curl "https://your-domain.com/v1/search?q=iogurte+natural" \
-H "Authorization: Bearer ntv_live_..."X-API-Key: ntv_live_... is also accepted. Never put an API key in browser source code.
3. Select a canonical result
curl "https://your-domain.com/v1/products/{id}?country=PT" \
-H "Authorization: Bearer ntv_live_..."What the platform does
Food search
Accent-insensitive, typo-tolerant ranking over name, brand, barcode and FoodEx2, with PT priority.
Barcode resolution
Cache-first lookup with Open Food Facts discovery and stale-while-revalidate refresh.
Nutrition
Core label values plus the full INSA vitamin, mineral, fatty-acid and composition superset.
Prices
Historical observations by retailer, physical store, country, currency and date—never an undated price claim.
Meals
Scale per-100g values by grams and return totals without treating missing nutrients as zero.
Recipes
Save named combinations, servings and instructions, then compute total and per-serving nutrition.
Classification
Browse the FoodEx2 hierarchy used to group generic and branded foods consistently.
Trust workflow
Verified brands, human review, source precedence, change detection and audit revisions.
Endpoint index
/v1/searchRanked Portugal-first food search/v1/autocompleteLightweight search suggestions/v1/products/{identifier}Complete food by UUID or barcode/v1/productsPropose a barcode product for review/v1/products/{identifier}/pricesDated store-price observations/v1/products/{identifier}/price-historyPrice history and aggregation/v1/products/{identifier}/pricesReport an in-store price/v1/products/{identifier}/imagesPropose a product image/v1/submissions/{id}Poll contribution review status/v1/products/{identifier}/relatedRelated products/v1/mealCalculate nutrition for weighed foods/v1/recipesSearch your recipes/v1/recipesCreate a recipe/v1/recipes/{id}Recipe and computed nutrition/v1/recipes/{id}Delete an owned recipe/v1/foodgroupsFoodEx2 classification tree/v1/import-urlQueue a product URL for reviewThe response contract
Canonical food data is flat JSON so mobile and server clients can read fields directly. Nutrient values use camelCase and are normalized per 100 g, or per 100 ml for liquids when the source defines that basis.
{
"data": {
"id": "uuid",
"source": "insa",
"sourceId": "TCA-123",
"attribution": "Data from INSA ...",
"confidence": "high",
"name": "Maçã com casca",
"brand": null,
"barcode": null,
"isGeneric": true,
"region": "PT",
"energyKcal": 64,
"proteinG": 0.2,
"vitaminCMg": 12,
"ingredientsText": null,
"allergens": null
}
}null means the source did not provide a reliable value. It is different from numeric zero.high is authoritative or verified; medium is sourced community/catalog data; low needs care.Source precedence
When the same barcode exists in several sources, the public API chooses: verified brand → verified manual → Open Food Facts → Nutripédia. INSA normally represents generic foods and does not collide with branded barcodes.
Search and autocomplete
/v1/searchSearches the canonical catalog first. Portugal is a ranking boost, not a hard filter. Set region=PT when only Portuguese records are acceptable.
| Parameter | Type | Behavior |
|---|---|---|
| q | string, required | Name, brand, barcode or category; maximum 200 characters |
| limit | integer 1–50 | Default 20 |
| offset | integer 0–1000 | Stable page offset |
| type | generic | branded | Restrict product type |
| source | source enum | Restrict provenance |
| region | string | Use PT for Portuguese-only data |
| category | string | Exact FoodEx2 level filter |
| fallback | auto | always | none | auto calls external discovery only when local results are empty |
| providers | comma-separated | off,wikipedia by default; fatsecret is explicit opt-in |
| language | pt | en | Wikipedia context language; default pt |
GET /v1/search?q=queijo+cottage®ion=PT&limit=10
{
"data": [/* canonical foods, each with searchScore */],
"externalData": [/* transient OFF food or Wikipedia context */],
"meta": {
"localCount": 10,
"enrichment": {
"requestedInBackground": true,
"reason": "catalog_revalidation"
}
}
}data records have NauTritiva UUIDs and can be used in meals and recipes. externalData is transient discovery and cannot be used as canonical nutrition until imported and sourced./v1/autocompleteReturns up to ten lightweight objects with id, name, brand and barcode. Use for typeahead; use /search only after submission so external providers are not hit per keystroke.
Products, barcodes, and related foods
/v1/products/{identifier}identifier may be a NauTritiva UUID or a barcode. A missing barcode is fetched from Open Food Facts, normalized and cached. Existing stale OFF data is returned immediately and refreshed after the response.
The response contains the complete canonical food, the newest PT price observations, and price-refresh status. Pass country=ALL to include other countries.
/v1/products/{identifier}/relatedReturns up to ten published foods from the same brand first, then the same FoodEx2 level. Every item uses the complete public food contract.
Product-specific fields
| Field | Type | Meaning |
|---|---|---|
| ingredientsText | string | null | Literal source ingredient declaration |
| allergens | string[] | null | Only explicitly declared allergens; never inferred |
| servingSize | string | null | Source serving description |
| imageUrl | URL | null | Product image when licensing permits |
| insaVersion | string | null | Composition-table release for INSA records |
Price observations
/v1/products/{identifier}/pricesDevolve a projeção de preço atual por artigo e localização quando existe uma correspondência canónica aceite. Durante a transição, produtos apenas nutricionais mantêm o formato legado. Este GET não agenda nem escreve recolhas.
{
"data": [{
"source": "open_prices",
"price": 2.77,
"currency": "EUR",
"retailer": "Auchan",
"storeName": "Auchan ...",
"countryCode": "PT",
"city": "Lisboa",
"observedAt": "2026-06-12",
"ageDays": 28,
"sourceUrl": "https://prices.openfoodfacts.org/..."
}],
"meta": { "projection": "legacy", "requestedInBackground": false }
}observedAt. Um resultado vazio significa que não existe ainda uma observação publicável.Como funciona a publicação
Artigos de retalhista, identidade canónica e nutrição permanecem separados. Só uma correspondência revista e aceite pode alimentar a projeção atual. Recolhas são iniciadas manualmente nesta fase e podem ser inspecionadas no painel administrativo.
O adaptador direto reutiliza um transporte HTTPS com allowlist, validação de redirects, robots, timeout e limite de tamanho. A fixture de demonstração não efetua pedidos de rede. A API nunca inventa promoções nem preços.
/v1/products/{identifier}/price-historyHistórico limitado por period=30d|90d|1y|all ou por from/to. Pode filtrar por retailer e location e agregar por day, week ou month. Valores monetários agregados são strings decimais.
/v1/products/{identifier}/pricesSubmit a shelf observation as source=user_report. It is shown with confidence=low, the API key is retained as provenance, and retries from the same key/store/day update instead of duplicating.
{
"price": 2.49,
"currency": "EUR",
"retailer": "Mercadona",
"storeName": "Mercadona Braga Centro",
"city": "Braga",
"observedAt": "2026-07-10",
"photoUrl": "https://example.com/shelf-photo.jpg"
}Meals and recipes
/v1/mealSend canonical food UUIDs and consumed grams. Each nutrient is scaled from its per-100g value.
{
"items": [
{ "foodId": "uuid-of-chicken", "grams": 180 },
{ "foodId": "uuid-of-rice", "grams": 120 }
]
}The response includes totals and incompleteFields. If one item has no iodine value, iodine is not silently counted as zero.
/v1/recipes{
"name": "Arroz de frango",
"description": "Family recipe",
"servings": 4,
"instructions": "Cook and combine.",
"ingredients": [
{ "foodId": "uuid", "grams": 500, "notes": "cooked" }
]
}GET /v1/recipes?q=... searches recipes owned by the API key. GET /v1/recipes/{id} calculates total and per-serving nutrition. Only the owning key may delete it.
Classification and reviewed import
/v1/foodgroupsLists distinct FoodEx2 L1/L2/L3 paths present in the catalog. Use them for filters, category browsing, analytics and interoperable EU classification.
/v1/productsPropose a shopper-scanned product. The record remains pending until an admin approves it.
{ "name": "Iogurte natural", "brand": "Marca", "barcode": "5601234567890", "category": "Dairy", "imageUrl": "https://example.com/label.jpg", "nutrition": { "energyKcal": 62, "proteinG": 3.8 } }/v1/products/{identifier}/imagesPropose an HTTPS image for an existing product through the same review queue.
{ "imageUrl": "https://example.com/front.jpg" }/v1/submissions/{id}Poll the pending, approved, or rejected status. A key can only see its own submissions. Use GET /v1/submissions for a cursor-paginated list.
/v1/import-urlQueues one HTTPS product URL for human review. Free keys may submit up to five per rolling 24 hours; paid keys use their normal plan limits. Include productIdentifier to connect an approved retailer URL to an existing product and its price-refresh workflow.
{ "url": "https://brand.example/product", "productIdentifier": "5601234567890" }Complete data dictionary
Identity, quality, and provenance
| Field | Type | Meaning |
|---|---|---|
| id | uuid | NauTritiva canonical identifier |
| source | enum | insa | off | brand | manual | nutripedia |
| sourceId | string | Identifier used by the upstream source |
| attribution | string | Required source and licence credit |
| confidence | enum | high | medium | low |
| status | enum | published | pending | flagged |
| name | string | Food or product name |
| brand | string | null | Brand when this is a branded product |
| barcode | string | null | GTIN/EAN/UPC when available |
| isGeneric | boolean | Generic composition food rather than a branded SKU |
| region | string | null | PT is prioritized; ES, FR, EU and others may follow |
| foodex2L1/L2/L3 | string | null | Hierarchical FoodEx2 classification |
| categoriesTags | string[] | null | Original normalized category tags |
| completenessScore | number | null | 0–1 coverage of the eight-field core panel |
| createdAt/updatedAt | ISO date-time | Canonical record timestamps |
| lastCheckedAt | ISO date-time | null | Last upstream nutrition validation |
Core nutrition
All values are number | null and per 100 g/ml.
| Field | Unit | Meaning |
|---|---|---|
| energyKcal | kcal | Energy |
| energyKj | kJ | Energy |
| fatG | g | Total fat |
| saturatedFatG | g | Saturated fat |
| carbsG | g | Carbohydrates |
| sugarsG | g | Sugars |
| fibreG | g | Fibre |
| proteinG | g | Protein |
| saltG | g | Salt |
| sodiumMg | mg | Sodium |
Extended composition
Generic INSA foods are usually rich in these values; branded labels commonly omit them. That difference is expected.
| Field | Unit |
|---|---|
| monounsaturatedFatG | g |
| polyunsaturatedFatG | g |
| linoleicAcidG | g |
| transFatG | g |
| oligosaccharidesG | g |
| starchG | g |
| alcoholG | g |
| waterG | g |
| organicAcidsG | g |
| ashG | g |
| cholesterolMg | mg |
| vitaminAUg | µg |
| betaCaroteneEqUg | µg |
| alphaCaroteneUg | µg |
| betaCaroteneUg | µg |
| betaCryptoxanthinUg | µg |
| lycopeneUg | µg |
| luteinUg | µg |
| zeaxanthinUg | µg |
| vitaminDUg | µg |
| alphaTocopherolMg | mg |
| thiaminMg | mg |
| riboflavinMg | mg |
| niacinMg | mg |
| niacinEqMg | mg |
| tryptophan60Mg | mg |
| vitaminB6Mg | mg |
| vitaminB12Ug | µg |
| vitaminCMg | mg |
| folateUg | µg |
| potassiumMg | mg |
| calciumMg | mg |
| phosphorusMg | mg |
| magnesiumMg | mg |
| ironMg | mg |
| zincMg | mg |
| seleniumUg | µg |
| iodineUg | µg |
Freshness and demand-driven enrichment
- 1. Search: the response is served from the local indexed catalog; demand is recorded after the response.
- 2. Discovery: popular and empty searches are processed first. PT OFF candidates are ordered first.
- 3. Safe insert: sparse search hits can create missing records but never overwrite a richer existing record.
- 4. Selection: choosing a stale OFF product schedules its full barcode refresh and a PT price refresh.
- 5. Verification: meaningful changes are revisioned; large changes enter human review rather than silently replacing data.
Errors, quotas, and integration guidance
| HTTP | Meaning | Action |
|---|---|---|
| 400 | Invalid or missing input | Correct query parameters or JSON |
| 401 | Missing or invalid API key | Send Bearer or X-API-Key |
| 403 | Tier does not allow the operation | Upgrade or use an allowed endpoint |
| 404 | Canonical item not found | Search, scan a barcode, or allow background discovery |
| 429 | Minute rate or monthly quota exceeded | Back off and inspect your plan |
| 5xx | Temporary server/upstream failure | Retry with exponential backoff |
- Cache canonical reads by UUID or barcode, but retain freshness metadata.
- Debounce autocomplete and submit full search only when the user commits the query.
- Use the canonical
idin meals and recipes, never an external fallback ID. - Render null as “not available,” not zero.
- Keep source and attribution alongside exported or displayed data.