Skip to content
opis.Nutrition API

Confirmation

Between identification and nutrition, a meal analysis can stop and wait for a person to accept, edit or reject the identified items. You choose per project — and can override per request — whether that happens:

  • automatic (the default): identified items go straight to nutrition. One request in, one result out.
  • manual: the analysis enters requires_confirmation, we send you a webhook, your app shows the items to its user, and you POST …/confirm with their answer. Nutrition runs on the confirmed items.

The wait is stored as durable workflow state with a deadline, not as a job in a queue: it survives deploys, can last up to 7 days, and you can list everything that is waiting.

Automatic confirmation

The default. Identified items go straight to nutrition.

  1. queued
    You POST /v1/meal-analyses; we answer 202.
  2. processing· identification
    Foods, portions and confidence are identified from the photo.
  3. processing· nutrition
    Confirmation is recorded as automatic; nutrients are computed.
  4. succeeded
    Webhook meal_analysis.succeeded; fetch the result.

Manual confirmation

A person reviews the items before nutrition runs. The wait is durable state — nothing sits in a queue.

  1. queued
    Same create, with confirmation mode: "manual".
  2. processing· identification
    Foods and portions are identified.
  3. requires_confirmation
    Webhook meal_analysis.requires_confirmation. Your user accepts, edits or rejects; you POST …/confirm with If-Match.
  4. processing· nutrition
    Nutrients are computed for the confirmed items.
  5. succeeded
    The result keeps both the AI view and the human view.
  • Rejected → canceled (rejected_at_confirmation).
  • No answer by confirmation.expires_at → your on_expiry policy: cancel (default) or auto_confirm (recorded as auto_on_expiry).

Choosing a mode

AutomaticManual
Integration effortLowest: create → resultA confirmation screen plus one extra call
Latency to nutrientsSecondsWhenever your user answers
AccuracyThe model's identification and portionsA person corrects names and grams before nutrition
Typical useLogging at scale, analytics, low-stakes estimatesFood diaries, clinical and research data, coaching
BillingIdentification + nutritionIdentification always; nutrition only if confirmed

If the numbers end up in front of a clinician, a coach or a researcher, prefer manual: the human view is recorded next to the AI view, so you can always show what the model saw and what the person confirmed.

Setting the policy

Project default. In the console, open Projects and edit the project's confirmation defaults: mode, how long to wait (expires_after), what to do on expiry (on_expiry), whether If-Match is required, and whether the image cross-check runs.

Per request. Override the default with confirmation on create:

JSON
{
  "images": [{ "file_id": "file_01j9…" }],
  "confirmation": {
    "mode": "manual",
    "expires_after": "PT24H",
    "on_expiry": "cancel"
  }
}
  • expires_after is an ISO 8601 duration in days, hours, minutes and seconds, between PT5M and P7D (for example PT30M, PT24H, P3D). Test mode also accepts down to PT10S.
  • on_expiry is cancel (default) or auto_confirm.
  • With mode: "automatic", leave expires_after and on_expiry out.

The resolved policy is frozen on the analysis when it is created. Changing the project default later never affects analyses that already exist.

Building a confirmation screen

  1. Listen for meal_analysis.requires_confirmation (or poll until that status).

  2. Fetch the analysis and keep its ETag:

    HTTP
    GET /v1/meal-analyses/mna_01j9…
    → 200 OK
      ETag: "3"

    Show identification.items: name, grams and confidence. alternative_names make good one-tap corrections. Highlight items when identification.flags contains low_identification_confidence. Show confirmation.expires_at if your users might come back later.

  3. Let your user act on the whole list — keep, rename, change grams, remove, add — and then accept, save the edits, or reject the meal.

  4. Send the answer from your backend with If-Match and an Idempotency-Key.

Your users never talk to the opis API directly: your backend proxies the confirmation, so API keys stay on your servers. Send confirmed_by with a pseudonymous reference to the person who confirmed — never an email address or phone number.

No food detected

If no food was found, the analysis still enters requires_confirmation in manual mode, with an empty item list and the no_food_detected flag, so your user can add what they ate or reject the meal. (In automatic mode it simply succeeds with an empty result.)

POST /v1/meal-analyses/{id}/confirm

The body is { action, items?, confirmed_by? }. items is the complete final list — declarative, not a patch.

actionitemsResult
acceptmust be absentIdentified items are used as they are → processing (nutrition)
editrequired, 1–30 itemsYour list is used → processing (nutrition)
rejectmust be absentcanceled with cancellation_reason: "rejected_at_confirmation"

Accept as identified

curl https://platform.opis.health/v1/meal-analyses/mna_01j9…/confirm \
  -H "Authorization: Bearer $OPIS_API_KEY" \
  -H "Content-Type: application/json" \
  -H 'If-Match: "3"' \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{ "action": "accept", "confirmed_by": "user_8f2c41" }'

Edit: the declarative list

Every item you send is kept; every identified item you leave out is removed.

JSON
{
  "action": "edit",
  "confirmed_by": "user_8f2c41",
  "items": [
    { "id": "itm_01j9aaaa…", "grams": 180 },
    { "id": "itm_01j9bbbb…", "name": "Brown rice" },
    { "name": "Ketchup", "grams": 15, "item_type": "condiment" }
  ]
}

In this example the person changed the chicken's grams, renamed the rice (its estimated grams are kept), removed a third identified item by leaving it out, and added ketchup.

ItemRule
With idKeeps that identified item. Fields you send replace the identified values; fields you omit keep them. Each id must exist in identification.items and appear once.
Without idAdds an item; name is required. Without grams, a standard portion or category default is used and the result is flagged portion_defaulted.
name1–200 characters after Unicode normalisation; control characters are removed.
grams0.1–5000. Grams you send always win.
volume_ml0–5000, for drinks.

An edit with an empty list is refused with use_reject_instead — use action: "reject" to discard the meal.

How confirmation shows up in the result

Each nutrition item records where it came from, so the AI view and the human view both stay auditable. identification itself is never modified after confirmation.

originMeaning
identifiedAccepted as identified
identified_editedAn identified item whose name or grams the person changed
added_at_confirmationAdded by the person
portion_sourceMeaning
vision_estimateThe model's estimate, unchanged
confirmedGrams sent in the confirmation
carried_overThe item was renamed without new grams; the estimate was kept
standard_portion, category_defaultNo grams were available; a default was used (portion_defaulted)

The confirmation object records method (manual, automatic or auto_on_expiry), action, confirmed_at and confirmed_by, and the timeline records the transition.

Concurrency: If-Match and ETag

Confirm, cancel and expiry can race. Exactly one wins:

  • Send the ETag from your GET as If-Match. If the analysis changed since, you get 412 precondition_failed with the current_state, and nothing happens.
  • Turn on Require If-Match in the project's confirmation defaults to make the header mandatory; requests without it then get 428 precondition_required.
  • Confirming an analysis that is no longer waiting returns 409 invalid_state with current_state (its status, cancellation_reason and confirmation_method). After expiry, the reason is confirmation_expired.
  • With an Idempotency-Key, a retried confirm replays the original response instead of failing. See Idempotency.

A successful confirm answers 202 with the updated analysis (processing, stage nutrition) and its new ETag, or the canceled analysis for a reject.

Expiry and reminders

When confirmation.expires_at passes without an answer, your on_expiry policy runs:

  • cancel (default): the analysis ends canceled with cancellation_reason: "confirmation_expired". Nutrition never runs; identification is still billed.
  • auto_confirm: the identified items are accepted as they are and nutrition runs. The record says method: "auto_on_expiry", so you can always tell an automatic acceptance from a human one.

Choose auto_confirm deliberately. If you picked manual mode because you need human-reviewed data, auto_confirm quietly fills the gaps with unreviewed results. Filter on confirmation.method if you do.

Reminders. Before the deadline — at expires_at minus one hour, or a quarter of the window for short windows — we send meal_analysis.confirmation_expiring once, so you can nudge your user.

Limits

  • Up to 10,000 analyses per organization can wait in requires_confirmation at once. Over the cap, creating a manual-mode analysis returns 429 too_many_pending_confirmations. Confirm, reject or cancel some first.
  • While an analysis waits, its images are held (even under the zero retention class) and its pipeline release stays pinned, so a confirmation days later is processed by exactly the same pipeline.

Testing confirmation

Test keys exercise the real confirmation gate. To test expiry quickly, create the analysis with "expires_after": "PT10S" (test mode only) and both policies. In the console's Analyses view, test-mode analyses that are waiting have Accept / Edit / Reject buttons, so you can try the flow before your own screen exists. See Test mode.