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 entersrequires_confirmation, we send you a webhook, your app shows the items to its user, and youPOST …/confirmwith 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.
- queuedYou
POST /v1/meal-analyses; we answer 202. - processing· identificationFoods, portions and confidence are identified from the photo.
- processing· nutritionConfirmation is recorded as automatic; nutrients are computed.
- succeededWebhook
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.
- queuedSame create, with confirmation
mode: "manual". - processing· identificationFoods and portions are identified.
- requires_confirmationWebhook
meal_analysis.requires_confirmation. Your user accepts, edits or rejects; youPOST …/confirmwithIf-Match. - processing· nutritionNutrients are computed for the confirmed items.
- succeededThe result keeps both the AI view and the human view.
- Rejected →
canceled(rejected_at_confirmation). - No answer by
confirmation.expires_at→ youron_expirypolicy:cancel(default) orauto_confirm(recorded asauto_on_expiry).
Choosing a mode
| Automatic | Manual | |
|---|---|---|
| Integration effort | Lowest: create → result | A confirmation screen plus one extra call |
| Latency to nutrients | Seconds | Whenever your user answers |
| Accuracy | The model's identification and portions | A person corrects names and grams before nutrition |
| Typical use | Logging at scale, analytics, low-stakes estimates | Food diaries, clinical and research data, coaching |
| Billing | Identification + nutrition | Identification 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:
{
"images": [{ "file_id": "file_01j9…" }],
"confirmation": {
"mode": "manual",
"expires_after": "PT24H",
"on_expiry": "cancel"
}
}expires_afteris an ISO 8601 duration in days, hours, minutes and seconds, betweenPT5MandP7D(for examplePT30M,PT24H,P3D). Test mode also accepts down toPT10S.on_expiryiscancel(default) orauto_confirm.- With
mode: "automatic", leaveexpires_afterandon_expiryout.
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
-
Listen for
meal_analysis.requires_confirmation(or poll until that status). -
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_namesmake good one-tap corrections. Highlight items whenidentification.flagscontainslow_identification_confidence. Showconfirmation.expires_atif your users might come back later. -
Let your user act on the whole list — keep, rename, change grams, remove, add — and then accept, save the edits, or reject the meal.
-
Send the answer from your backend with
If-Matchand anIdempotency-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.
action | items | Result |
|---|---|---|
accept | must be absent | Identified items are used as they are → processing (nutrition) |
edit | required, 1–30 items | Your list is used → processing (nutrition) |
reject | must be absent | canceled 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.
{
"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.
| Item | Rule |
|---|---|
With id | Keeps 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 id | Adds an item; name is required. Without grams, a standard portion or category default is used and the result is flagged portion_defaulted. |
name | 1–200 characters after Unicode normalisation; control characters are removed. |
grams | 0.1–5000. Grams you send always win. |
volume_ml | 0–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.
origin | Meaning |
|---|---|
identified | Accepted as identified |
identified_edited | An identified item whose name or grams the person changed |
added_at_confirmation | Added by the person |
portion_source | Meaning |
|---|---|
vision_estimate | The model's estimate, unchanged |
confirmed | Grams sent in the confirmation |
carried_over | The item was renamed without new grams; the estimate was kept |
standard_portion, category_default | No 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
ETagfrom yourGETasIf-Match. If the analysis changed since, you get412 precondition_failedwith thecurrent_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_statewithcurrent_state(itsstatus,cancellation_reasonandconfirmation_method). After expiry, the reason isconfirmation_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 endscanceledwithcancellation_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 saysmethod: "auto_on_expiry", so you can always tell an automatic acceptance from a human one.
Choose
auto_confirmdeliberately. If you picked manual mode because you need human-reviewed data,auto_confirmquietly fills the gaps with unreviewed results. Filter onconfirmation.methodif 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_confirmationat once. Over the cap, creating a manual-mode analysis returns429 too_many_pending_confirmations. Confirm, reject or cancel some first. - While an analysis waits, its images are held (even under the
zeroretention 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.