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.

Portugal first

PT records rank ahead of equally relevant foreign records. Global data remains available as fallback.

Traceable

Every record carries source, confidence, attribution, timestamps, and nulls for genuinely missing values.

Demand driven

Searches and product views queue background discovery, nutrition refresh, and price synchronization.

Core rule
Missing values are 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

GET/v1/searchRanked Portugal-first food search
GET/v1/autocompleteLightweight search suggestions
GET/v1/products/{identifier}Complete food by UUID or barcode
POST/v1/productsPropose a barcode product for review
GET/v1/products/{identifier}/pricesDated store-price observations
GET/v1/products/{identifier}/price-historyPrice history and aggregation
POST/v1/products/{identifier}/pricesReport an in-store price
POST/v1/products/{identifier}/imagesPropose a product image
GET/v1/submissions/{id}Poll contribution review status
GET/v1/products/{identifier}/relatedRelated products
POST/v1/mealCalculate nutrition for weighed foods
GET/v1/recipesSearch your recipes
POST/v1/recipesCreate a recipe
GET/v1/recipes/{id}Recipe and computed nutrition
DELETE/v1/recipes/{id}Delete an owned recipe
GET/v1/foodgroupsFoodEx2 classification tree
POST/v1/import-urlQueue a product URL for review

The 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
null means the source did not provide a reliable value. It is different from numeric zero.
confidence
high is authoritative or verified; medium is sourced community/catalog data; low needs care.
attribution
Display or retain attribution when the upstream licence requires it, especially for OFF records.

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.

Products, barcodes, and related foods

GET/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.

GET/v1/products/{identifier}/related

Returns 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

FieldTypeMeaning
ingredientsTextstring | nullLiteral source ingredient declaration
allergensstring[] | nullOnly explicitly declared allergens; never inferred
servingSizestring | nullSource serving description
imageUrlURL | nullProduct image when licensing permits
insaVersionstring | nullComposition-table release for INSA records

Price observations

GET/v1/products/{identifier}/prices

Devolve 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 }
}
Significado do preço
Um preço é uma observação numa loja e data, não uma garantia do preço de prateleira atual. Mostre sempre 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.

GET/v1/products/{identifier}/price-history

Histó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.

POST/v1/products/{identifier}/prices

Submit 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

POST/v1/meal

Send 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.

POST/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

GET/v1/foodgroups

Lists distinct FoodEx2 L1/L2/L3 paths present in the catalog. Use them for filters, category browsing, analytics and interoperable EU classification.

POST/v1/products

Propose 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 } }
POST/v1/products/{identifier}/images

Propose an HTTPS image for an existing product through the same review queue.

{ "imageUrl": "https://example.com/front.jpg" }
GET/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.

POST/v1/import-url

Queues 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

FieldTypeMeaning
iduuidNauTritiva canonical identifier
sourceenuminsa | off | brand | manual | nutripedia
sourceIdstringIdentifier used by the upstream source
attributionstringRequired source and licence credit
confidenceenumhigh | medium | low
statusenumpublished | pending | flagged
namestringFood or product name
brandstring | nullBrand when this is a branded product
barcodestring | nullGTIN/EAN/UPC when available
isGenericbooleanGeneric composition food rather than a branded SKU
regionstring | nullPT is prioritized; ES, FR, EU and others may follow
foodex2L1/L2/L3string | nullHierarchical FoodEx2 classification
categoriesTagsstring[] | nullOriginal normalized category tags
completenessScorenumber | null0–1 coverage of the eight-field core panel
createdAt/updatedAtISO date-timeCanonical record timestamps
lastCheckedAtISO date-time | nullLast upstream nutrition validation

Core nutrition

All values are number | null and per 100 g/ml.

FieldUnitMeaning
energyKcalkcalEnergy
energyKjkJEnergy
fatGgTotal fat
saturatedFatGgSaturated fat
carbsGgCarbohydrates
sugarsGgSugars
fibreGgFibre
proteinGgProtein
saltGgSalt
sodiumMgmgSodium

Extended composition

Generic INSA foods are usually rich in these values; branded labels commonly omit them. That difference is expected.

FieldUnit
monounsaturatedFatGg
polyunsaturatedFatGg
linoleicAcidGg
transFatGg
oligosaccharidesGg
starchGg
alcoholGg
waterGg
organicAcidsGg
ashGg
cholesterolMgmg
vitaminAUgµg
betaCaroteneEqUgµg
alphaCaroteneUgµg
betaCaroteneUgµg
betaCryptoxanthinUgµg
lycopeneUgµg
luteinUgµg
zeaxanthinUgµg
vitaminDUgµg
alphaTocopherolMgmg
thiaminMgmg
riboflavinMgmg
niacinMgmg
niacinEqMgmg
tryptophan60Mgmg
vitaminB6Mgmg
vitaminB12Ugµg
vitaminCMgmg
folateUgµg
potassiumMgmg
calciumMgmg
phosphorusMgmg
magnesiumMgmg
ironMgmg
zincMgmg
seleniumUgµg
iodineUgµg

Freshness and demand-driven enrichment

  1. 1. Search: the response is served from the local indexed catalog; demand is recorded after the response.
  2. 2. Discovery: popular and empty searches are processed first. PT OFF candidates are ordered first.
  3. 3. Safe insert: sparse search hits can create missing records but never overwrite a richer existing record.
  4. 4. Selection: choosing a stale OFF product schedules its full barcode refresh and a PT price refresh.
  5. 5. Verification: meaningful changes are revisioned; large changes enter human review rather than silently replacing data.
FatSecret policy
FatSecret is not a default provider. It can be requested explicitly, but its search-result storage restrictions mean it cannot be used to grow the canonical NauTritiva catalog.

Errors, quotas, and integration guidance

HTTPMeaningAction
400Invalid or missing inputCorrect query parameters or JSON
401Missing or invalid API keySend Bearer or X-API-Key
403Tier does not allow the operationUpgrade or use an allowed endpoint
404Canonical item not foundSearch, scan a barcode, or allow background discovery
429Minute rate or monthly quota exceededBack off and inspect your plan
5xxTemporary server/upstream failureRetry 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 id in meals and recipes, never an external fallback ID.
  • Render null as “not available,” not zero.
  • Keep source and attribution alongside exported or displayed data.