Async workflows
Meal analyses and nutrition estimates are workflows: long-running resources that move through a
small set of statuses. Creating one never blocks on the AI pipeline — you get 202 Accepted
immediately and follow the resource until it settles.
Statuses
| Status | Terminal | Meaning |
|---|---|---|
queued | no | Accepted; waiting for its files to be ready or for capacity |
processing | no | A stage is running. progress.stage is identification or nutrition |
requires_confirmation | no | Manual confirmation only. Waiting for POST …/confirm until confirmation.expires_at |
succeeded | yes | Nutrition is done — or, in automatic mode, no food was detected |
failed | yes | A stage failed with no retries left; error.stage and error.code say which and why |
canceled | yes | cancellation_reason is requested, rejected_at_confirmation or confirmation_expired |
Terminal statuses never change. requires_confirmation is settled but not terminal: nothing
more happens until your user answers or the deadline passes. Treat it as a normal outcome, not an
error. Nutrition estimates have no confirmation, so they never enter requires_confirmation.
queued → processing (identification) → [requires_confirmation] → processing (nutrition) → succeeded
↘ failed ↘ canceled ↘ failedCreating: 202, Location and Retry-After
HTTP/1.1 202 Accepted
Location: https://platform.opis.health/v1/meal-analyses/mna_01j9…
Retry-After: 4
Request-Id: req_01j9…
Opis-Pipeline-Release: 2026-10.1
Content-Type: application/json
{ "id": "mna_01j9…", "object": "meal_analysis", "status": "queued", … }Retry-After is our estimate, in seconds, of when the next settled state is likely. Every response
also carries a Request-Id (quote it to support) and the Opis-Pipeline-Release that is processing
the workflow.
Waiting in the request: Prefer: wait
Add Prefer: wait=N (N ≤ 25 seconds) to a create or a GET. The API holds the response until the
workflow settles — succeeded, failed, canceled or requires_confirmation — and then answers
200 with Preference-Applied: wait. If N seconds pass first, you get the usual 202 and carry on
polling or wait for a webhook.
curl https://platform.opis.health/v1/meal-analyses \
-H "Authorization: Bearer $OPIS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Prefer: wait=25" \
-d '{ "images": [{ "file_id": "file_01j9…" }] }'In automatic mode most analyses settle inside the window; standalone nutrition estimates typically
finish in 3–10 seconds. Prefer: wait is best-effort: under load the header may be ignored, and you
get 202 straight away.
Polling etiquette
- Prefer webhooks in production. Poll as a fallback, or for the first few seconds of an interactive flow.
- Honour
Retry-After. If you poll without it, start at 2 seconds and back off exponentially to at most 30 seconds. - Use conditional requests. Responses carry an
ETag; send it back asIf-None-Matchand an unchanged resource costs you a304with no body. - Stop at a settled state. Stop polling at
requires_confirmation,succeeded,failedorcanceled. - Stay inside the rate limit. Polling counts towards it. See Rate limits.
Reading progress
progress.stagetells you which stage is running:identification,confirmationornutrition.identificationappears as soon as identification succeeds — atrequires_confirmationin manual mode, or duringprocessing(nutrition) in automatic mode. It never changes after confirmation.nutritionappears when the workflow succeeds.GET /v1/meal-analyses/{id}/eventsreturns the workflow timeline: created, identified, requires confirmation, confirmed (by whom and how), nutrition started, succeeded… Payloads are never included, only the transitions.
Cancelling and deleting
POST /v1/meal-analyses/{id}/cancelstops a workflow in any non-terminal status. It ends ascanceledwithcancellation_reason: "requested". A stage that already finished is still billed.DELETE /v1/meal-analyses/{id}erases the workflow, its images and its results, including the copies held by the nutrition engine. Use it to honour your users' erasure requests.
The same operations exist for /v1/nutrition-estimates.
Listing
GET /v1/meal-analyses returns newest first with cursor pagination: limit (1–100, default 20)
and starting_after (the next_cursor of the previous page); the response has has_more and
next_cursor. Filter with status, external_id, created_gte and created_lte.
Failures
A failed workflow carries an error object:
{
"status": "failed",
"error": {
"type": "https://platform.opis.health/docs/errors/upstream_unavailable",
"code": "upstream_unavailable",
"stage": "identification",
"retryable": true
}
}Transient upstream problems are retried inside the platform before a workflow fails, so a failed
status is final for that workflow. If retryable is true, creating a new workflow with the same
inputs is reasonable. Failures caused on our side (upstream_unavailable, processing_timeout,
internal_error) are never billed.