Webhooks
A workspace's endpoints receive a signed event when a batch ends, in OpenAI's webhook format, so OpenAI's webhook verifiers work. Endpoints are set up in the Console, with the events each receives and a signing secret shown once. No endpoint, no webhook: polling the batch always works. The events come only from batches, which are not offered in production yet; the endpoint tools in the Console are live.
| Event | When |
|---|---|
batch.completed | The batch entered completed |
batch.failed | The batch entered failed |
batch.expired | The batch entered expired |
batch.cancelled | The batch entered cancelled |
webhook.test | A test send from the Console |
- Event
batch.completed- When
- The batch entered
completed
- Event
batch.failed- When
- The batch entered
failed
- Event
batch.expired- When
- The batch entered
expired
- Event
batch.cancelled- When
- The batch entered
cancelled
- Event
webhook.test- When
- A test send from the Console
Event
{"object": "event", "id": "evt_...", "type": "batch.completed", "created_at": 1792663200, "data": {"id": "batch_..."}}The payload names the batch; read the batch for its state. One event per batch, emitted when it enters that terminal status.
Parts list: Standard Webhooks headers
webhook-idThe
evt_id, the same on every retry and every endpoint: deduplicate by it.webhook-timestampUnix seconds. Reject a timestamp outside your own tolerance.
webhook-signaturev1,followed by the base64 of an HMAC-SHA256 over<webhook-id>.<webhook-timestamp>.<body>, keyed with the secret's bytes (the secret iswhsec_and base64). While a rotated secret overlaps, two signatures separated by a space.
Verify
import base64, hashlib, hmac, time def verified(signing_secret: str, headers: dict, body: bytes, tolerance_s: int) -> bool: # signing_secret: the endpoint's secret from the Console, whsec_ and base64. secret = base64.b64decode(signing_secret.removeprefix("whsec_")) msg_id = headers["webhook-id"] timestamp = headers["webhook-timestamp"] if abs(time.time() - int(timestamp)) > tolerance_s: return False signed = f"{msg_id}.{timestamp}.".encode() + body expected = base64.b64encode(hmac.new(secret, signed, hashlib.sha256).digest()).decode() return any( part.startswith("v1,") and hmac.compare_digest(part[3:], expected) for part in headers["webhook-signature"].split(" ") )- A
POSTwithContent-Type: application/json. A 2xx within the delivery timeout Not decided yet delivers; anything else, a redirect included (never followed), is retried with exponential backoff and jitter for up to a retry window Not decided yet, then recorded as failed in the endpoint's delivery log. - At least once; the order of deliveries is not guaranteed. Order by the payload's
created_atonly to the second, and read the batch when the order matters. - An endpoint that keeps failing is never switched off: it is probed until its first success.
- The sender refuses any host that resolves to a private, loopback, link-local or metadata address.
- Add an endpoint: an
httpsURL on a public host and the events it receives; the signing secret is shown once. - Send a test:
webhook.test, or a sample of one of the endpoint's events, marked as a test and naming no batch. - Rotate the secret: the retired secret keeps signing beside the new one for an overlap, which you may end sooner.
- The deliveries log, each with its attempts; redeliver one, redeliver the failed deliveries of a period after an outage, or recover an endpoint that backs off once its receiver is fixed.
Related sheets
- Sheet
- 17 of 19
- Detail
- M
- Updated
- 2026-11-11