Skip to main content
Pass a webhook URL when you submit a request and ModelRunner POSTs the result to it when the request settles. No polling loop, no long-lived connection, and nothing lost if your process restarts mid-request. Best for server-to-server integrations and long jobs (video, training). For a browser UI that shows several requests at once, the SSE stream is usually a better fit.

Attaching a webhook

Add the reserved top-level webhook key to the submit body, alongside the model’s own input fields:
The create response echoes both back so you can confirm they were accepted.
An unusable webhook fails the submit with a 400 rather than being silently dropped — you find out immediately instead of waiting for a callback that can never arrive. Rejected: a non-HTTPS scheme, a malformed URL, credentials embedded in the URL, a host that resolves to a private or internal address, an unknown event name, or webhook_events_filter without a webhook.

Events

start is best-effort — never block on it. A fast request can go from IN_QUEUE straight to a terminal state between two provider polls, in which case only completed is delivered. This is inherent to how status is observed, not a bug.
There are no incremental output or logs events: ModelRunner does not stream partial output into the request record, so there is nothing to emit them from. Use completed and read the payload.

The payload

The body is the same object response_url returns, plus event and billingStatus. One shape to learn.
Check billingStatus, not just status. A generation that failed at the provider is recorded as status: "COMPLETED" with billingStatus: "failed" and a populated error — because the request itself completed, it just produced no output. Treating status: "COMPLETED" alone as success will report every failure as a success.A real success is status: "COMPLETED" with billingStatus of charged or partial.
Any metadata you attached at submit time is echoed back, which is the easiest way to correlate a delivery with your own records without a database lookup.

Verifying a delivery

Every delivery is signed using Standard Webhooks, so you can verify it with an off-the-shelf library rather than hand-rolled crypto. Three headers are sent:

Get your signing secret

The secret is per-account. Cache it — do not fetch it on every delivery.

Verify with a library

Verify manually

If you would rather not add a dependency: HMAC-SHA256 over {webhook-id}.{webhook-timestamp}.{rawBody}, keyed by the base64 portion of the secret after the whsec_ prefix.
The header carries more than one signature during a secret rotation. Always iterate.

Retries and idempotency

A delivery succeeds on any 2xx returned within 15 seconds. Anything else — including a 3xx, since redirects are not followed — counts as a failure and is retried on a fixed schedule:
That is 10 attempts spanning roughly two hours. Two conditions stop retries immediately: a 410 Gone, and a URL that resolves to a private address.
Delivery is at-least-once, so make your handler idempotent. A POST can succeed while our record of it fails to commit, and the delivery is then retried. webhook-id is stable across every retry of the same delivery — use it as your deduplication key.Also ignore anything that arrives after a terminal event for a request, and do not assume ordering between start and completed.

Requirements for your endpoint

  • HTTPS, on a publicly resolvable host. Private, loopback, link-local and internal addresses are rejected — both when you submit and again at delivery time against the resolved address.
  • Respond 2xx within 15 seconds. Acknowledge first and process asynchronously; do not do the work inside the request.
  • Redirects are not followed. Point the webhook at its final URL.

Inspecting deliveries

Each record carries status (pending, delivering, delivered, failed), attempts, lastResponseStatus and lastError — enough to tell a broken endpoint from a broken payload.

Rotating your secret

The new secret is returned and takes effect immediately. The previous secret keeps verifying for 24 hours, and during that window deliveries are signed with both — so you can roll the new value out without dropping anything. This is why your verification must iterate over every signature in the header.