Skip to content
opis.Nutrition API

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

StatusTerminalMeaning
queuednoAccepted; waiting for its files to be ready or for capacity
processingnoA stage is running. progress.stage is identification or nutrition
requires_confirmationnoManual confirmation only. Waiting for POST …/confirm until confirmation.expires_at
succeededyesNutrition is done — or, in automatic mode, no food was detected
failedyesA stage failed with no retries left; error.stage and error.code say which and why
canceledyescancellation_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.

Text
queued → processing (identification) → [requires_confirmation] → processing (nutrition) → succeeded
                    ↘ failed                       ↘ canceled                       ↘ failed

Creating: 202, Location and Retry-After

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

Shell
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 as If-None-Match and an unchanged resource costs you a 304 with no body.
  • Stop at a settled state. Stop polling at requires_confirmation, succeeded, failed or canceled.
  • Stay inside the rate limit. Polling counts towards it. See Rate limits.

Reading progress

  • progress.stage tells you which stage is running: identification, confirmation or nutrition.
  • identification appears as soon as identification succeeds — at requires_confirmation in manual mode, or during processing (nutrition) in automatic mode. It never changes after confirmation.
  • nutrition appears when the workflow succeeds.
  • GET /v1/meal-analyses/{id}/events returns 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}/cancel stops a workflow in any non-terminal status. It ends as canceled with cancellation_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:

JSON
{
  "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.