Skip to content
opis.Nutrition API

Webhooks

Webhooks tell your server when something happens — an analysis needs confirmation, a result is ready — so you do not have to poll. Deliveries follow the Standard Webhooks (opens in a new tab) specification: signed with HMAC-SHA256, retried with backoff, and verifiable with the official open-source libraries.

Setting up an endpoint

In the console, open Webhooks → Add endpoint:

  • URL: a public https endpoint on your side.
  • Project and mode: test endpoints receive events from test keys only; live endpoints from live keys only.
  • Events: subscribe only to what you handle.

The endpoint's signing secret (whsec_…) is shown once. Store it next to your API key. You can disable an endpoint, delete it, read its delivery log and send a test event from the console.

Events

EventWhen
meal_analysis.requires_confirmationManual mode: identification finished; show the items to your user
meal_analysis.confirmation_expiringManual mode: the confirmation deadline is near (sent once)
meal_analysis.confirmedThe analysis was confirmed (manual, automatic or auto_on_expiry). Opt-in
meal_analysis.succeededNutrition finished; the result is ready
meal_analysis.failedA stage failed with no retries left
meal_analysis.canceledCanceled: requested, rejected at confirmation, or confirmation expired
meal_analysis.revision.succeededA correction after success produced a new nutrition result
nutrition_estimate.succeededA standalone estimate finished
nutrition_estimate.failedA standalone estimate failed
file.rejectedAn uploaded image failed sanitisation
usage.threshold_reachedA usage quota reached 80 % or 100 %
pipeline.deprecatedA pipeline release you use was deprecated (with its sunset date)
api_key.auto_revokedA key was revoked automatically (leaked, or sent in a query string)

Payloads are thin

Events carry identifiers and status, never nutrients, images or your metadata values. Fetch the resource for the full picture — that also guarantees you read its latest state.

JSON
{
  "type": "meal_analysis.requires_confirmation",
  "timestamp": "2026-10-03T12:04:31.512Z",
  "data": {
    "id": "mna_01j9…",
    "object": "meal_analysis",
    "status": "requires_confirmation",
    "version": 3,
    "progress": { "stage": "confirmation" },
    "project_id": "proj_01j9…",
    "mode": "live",
    "external_id": "meal-123",
    "confirmation_expires_at": "2026-10-04T12:04:31.000Z",
    "confirmation_method": null,
    "cancellation_reason": null,
    "error_code": null,
    "revision": null
  }
}

Each request carries three headers:

HTTP
webhook-id: msg_2mR6…            (unique per event; identical on retries)
webhook-timestamp: 1759493071    (Unix seconds)
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=

Verifying signatures

Always verify before trusting a payload. Use the raw request body — re-serialised JSON will not match — and reject timestamps more than five minutes old (the libraries do this for you).

// npm install standardwebhooks
import express from 'express';
import { Webhook } from 'standardwebhooks';

const wh = new Webhook(process.env.OPIS_WEBHOOK_SECRET!); // "whsec_…"
const app = express();

app.post(
  '/webhooks/opis',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    let event: { type: string; data: { id: string; status: string } };
    try {
      event = wh.verify(req.body, {
        'webhook-id': req.header('webhook-id') ?? '',
        'webhook-timestamp': req.header('webhook-timestamp') ?? '',
        'webhook-signature': req.header('webhook-signature') ?? '',
      }) as typeof event;
    } catch {
      return res.status(400).send('Invalid signature');
    }

    res.status(204).end(); // acknowledge first…
    void handleEvent(req.header('webhook-id')!, event); // …then process asynchronously
  },
);

Other languages: the Standard Webhooks project maintains verifiers for Go, Java, Kotlin, C#, PHP, Ruby, Rust and Elixir. The scheme is: base64-decode the secret after whsec_, compute HMAC-SHA256(secret, "{webhook-id}.{webhook-timestamp}.{raw body}"), base64-encode it, and compare it in constant time with each v1, entry of webhook-signature (there may be several during secret rotation).

Handling deliveries

  • Answer fast. Return any 2xx within a few seconds and do the work in a background job. Slow or non-2xx answers count as failures.
  • Deduplicate. Delivery is at-least-once. Use webhook-id as an idempotency key in your handler.
  • Don't rely on order. Events for one analysis are usually in order but this is not guaranteed. Each payload carries status, version and timestamp; when in doubt, fetch the resource.
  • Retries. Failed deliveries are retried with exponential backoff over several days. An endpoint that keeps failing is disabled and your organization's owners are notified; re-enable it in the console once it is fixed.
  • Reconcile. If your endpoint was down, list resources by status (for example GET /v1/meal-analyses?status=requires_confirmation) to catch up.

Testing

  • Send test event in the console delivers a signed sample to the endpoint and shows the result in the delivery log.
  • Test-mode analyses emit the same events as live ones, to your test endpoints. Combine them with magic inputs to exercise failures and confirmation in CI.
  • For local development, expose your handler with a tunnel (for example cloudflared or ngrok) and register the tunnel URL as a test endpoint.