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
httpsendpoint on your side. - Project and mode:
testendpoints receive events from test keys only;liveendpoints 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
| Event | When |
|---|---|
meal_analysis.requires_confirmation | Manual mode: identification finished; show the items to your user |
meal_analysis.confirmation_expiring | Manual mode: the confirmation deadline is near (sent once) |
meal_analysis.confirmed | The analysis was confirmed (manual, automatic or auto_on_expiry). Opt-in |
meal_analysis.succeeded | Nutrition finished; the result is ready |
meal_analysis.failed | A stage failed with no retries left |
meal_analysis.canceled | Canceled: requested, rejected at confirmation, or confirmation expired |
meal_analysis.revision.succeeded | A correction after success produced a new nutrition result |
nutrition_estimate.succeeded | A standalone estimate finished |
nutrition_estimate.failed | A standalone estimate failed |
file.rejected | An uploaded image failed sanitisation |
usage.threshold_reached | A usage quota reached 80 % or 100 % |
pipeline.deprecated | A pipeline release you use was deprecated (with its sunset date) |
api_key.auto_revoked | A 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.
{
"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:
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
2xxwithin 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-idas 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,versionandtimestamp; 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 exampleGET /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
cloudflaredorngrok) and register the tunnel URL as a test endpoint.