SPB Git forge

spb/doc-api

Public
2commits 1branches 0releases
15.7 MBsize
maindefault branch
13 days agolast push
Python 88.3% TypeScript 7.6% Shell 4.1%
9.9 KB

# OpenAI Webhooks — atlas

Status: DOCUMENTED + LIVE_VERIFIED for GET /v1/webhook_endpoints (200, empty list) and GET /v1/webhook_event_types (200, 24 types) with our project key on 2026-09-18; endpoint create/update/delete/rotate/test are DOCUMENTED only (never called — rotation is destructive). Signature verification is LIVE_VERIFIED offline (SDK webhooks.unwrap + manual HMAC, Python and TypeScript). Sources: Webhooks guide · Webhook events reference · Agents API session webhooks · Video generation → webhooks · OpenAPI /webhook_endpoints*, /webhook_event_types, Webhook* schemas · SDK source openai/lib/_webhooks.py (3.16.2), openai 7.18.0 resources/webhooks.js · Standard Webhooks spec. Last verified: 2026-09-18. Machine-readable: generated/fragments/endpoints/openai-webhooks.json, parameters/openai-webhooks.json, webhook-events/openai-webhook-events.json (28 event records).

# 1. Delivery model

Aspect Behaviour
Scope Webhook endpoints are per project (dashboard Settings → Project → Webhooks or the API below, which authenticates with a normal project key — no Admin key needed).
Request POST <your https URL> with content-type: application/json, user-agent: OpenAI/1.0 (+https://platform.openai.com/docs/webhooks), headers webhook-id, webhook-timestamp, webhook-signature.
Envelope {"object":"event","id":"evt_…","type":"<event>","created_at":<unix s>,"data":{…}} — data is a thin pointer (usually just id); fetch the resource afterwards.
Acknowledge Return any 2xx within a few seconds; do the work asynchronously. 3xx are not followed (treated as failures).
Retries Non-2xx / timeout → retried with exponential backoff for up to 72 hours.
Duplicates Possible; deduplicate on webhook-id.
Secret whsec_… signing secret shown once at creation (signing_secret) or rotation; afterwards only signing_secret_hint is visible.
Local testing Needs a public URL (ngrok, Codespaces, Cloudflare Workers, …); the dashboard and POST /v1/webhook_endpoints/{id}/test send sample events.

# 2. Management API (/v1/webhook_endpoints)

Method Path Body / query Returns Status
GET /v1/webhook_endpoints limit (default 20), after {object:"list", data:[webhook_endpoint], first_id, last_id, has_more} LIVE_VERIFIED (200, data: [])
POST /v1/webhook_endpoints name* (1–256), url* (^https://, ≤ 2048), event_types*[] (≥ 1, from the project enum) webhook_endpoint with signing_secret DOCUMENTED
GET /v1/webhook_endpoints/{id} — webhook_endpoint (signing_secret_hint only) DOCUMENTED
POST /v1/webhook_endpoints/{id} any of name, url, event_types (full replacement set) webhook_endpoint DOCUMENTED
DELETE /v1/webhook_endpoints/{id} — {object:"webhook_endpoint.deleted", id, deleted:true} DOCUMENTED
POST /v1/webhook_endpoints/{id}/rotate_secret keep_old_secret_active_for_24_hours (bool, default false → old secret dies immediately) webhook_endpoint with new signing_secret DOCUMENTED (destructive, not called)
POST /v1/webhook_endpoints/{id}/test event_type* {object:"webhook_endpoint.test", webhook_endpoint_id, event_type, status_code, success:true} — success is always true; inspect status_code DOCUMENTED
GET /v1/webhook_event_types — {object:"list", data:[string]} LIVE_VERIFIED

webhook_endpoint object: id, object:"webhook_endpoint", created_at, updated_at (config or secret change only; tests don't bump it), name, url, event_types[], signing_secret_hint, [signing_secret].

SDK: Python client.webhooks.create/retrieve/update/list/delete/rotate_secret/test, client.webhooks.event_types.list(); Node client.webhooks.*, client.webhooks.eventTypes.list().

# 3. Event types catalogue

Live list returned by GET /v1/webhook_event_types on 2026-09-18 (24): batch.completed, batch.failed, batch.expired, batch.cancelled, response.completed, response.failed, response.cancelled, response.incomplete, eval.run.succeeded, eval.run.failed, eval.run.canceled, fine_tuning.job.succeeded, fine_tuning.job.failed, fine_tuning.job.cancelled, realtime.call.incoming, video.completed, video.failed, safety.alert.created, agent.session.created, agent.session.action_required, agent.session.in_progress, agent.session.idle, agent.session.failed, live.transport.incoming.

Event When data Status
response.completed / .failed / .cancelled / .incomplete background Responses (background: true) reach a terminal state {id: "resp_…"} → GET /v1/responses/{id} DOCUMENTED, LIVE_VERIFIED (listed)
batch.completed / .failed / .expired / .cancelled Batch API job terminal states {id: "batch_…"} DOCUMENTED, LIVE_VERIFIED
fine_tuning.job.succeeded / .failed / .cancelled fine-tuning job terminal states {id: "ftjob_…"} DOCUMENTED, LIVE_VERIFIED
eval.run.succeeded / .failed / .canceled (note US spelling) eval run terminal states {id: "evalrun_…"} DOCUMENTED, LIVE_VERIFIED
realtime.call.incoming inbound SIP call awaiting Realtime accept/reject `{call_id: "rtc_…", sip_headers:[{name,value}], sip_media_security?: "rtp" "srtp"
live.transport.incoming same pending SIP session offered to the Live API {type:"sip", session_id:"live_…", sip_headers[], sip_media_security?} DOCUMENTED, LIVE_VERIFIED
live.call.incoming DEPRECATED predecessor of live.transport.incoming; new subscriptions refused {session_id, sip_headers, sip_media_security} DOCUMENTED, DEPRECATED (not in live list)
video.completed / video.failed video generation job finished {id: "video_…"} DOCUMENTED (video guide), LIVE_VERIFIED
safety.alert.created approved safety alert for the project {id: "alert_…"} → GET /v1/safety/alerts/{id} DOCUMENTED, LIVE_VERIFIED
safety.org_alert.created alert for an enterprise workspace {id} DOCUMENTED (not project-subscribable in our list)
safety.warning_issued / safety.deactivation_issued safety identifier warning / deactivation {id: "C-…"} → GET /v1/safety/cases/{id} DOCUMENTED (not in our live list)
agent.session.created / .action_required / .in_progress / .idle / .failed Agents API sessions (beta) created: {id:"sess_…", environment_id, environment_type, connect{remote_url}}; action_required: `{id, required_action{type: function_call environment_connection}}; others {id}`

Payloads never contain outputs — always re-fetch with your API key (data-residency: background: true is unavailable in the EU region, so response.* webhooks are US/global only).

# 4. Signature verification (Standard Webhooks)

text
signed_content = webhook-id + "." + webhook-timestamp + "." + raw_body
key            = base64_decode(secret minus the "whsec_" prefix)
signature      = base64( HMAC-SHA256(key, signed_content) )
webhook-signature: v1,<signature>[ v1,<signature2> …]      # any match passes; two entries during rotation
reject when |now − webhook-timestamp| > tolerance (SDK default 300 s)

Verified facts (from SDK sources, exercised offline): the body must be the raw bytes (never re-serialise); comparison is constant-time; Python raises openai.InvalidWebhookSignatureError, Node OpenAI.InvalidWebhookSignatureError; missing headers raise before HMAC.

Language SDK Manual
Python client = OpenAI(webhook_secret=…); event = client.webhooks.unwrap(request.data, request.headers) (typed UnwrapWebhookEvent) or client.webhooks.verify_signature(payload, headers, secret=…, tolerance=300) examples/openai/webhooks/verify_manual.py
Node await client.webhooks.unwrap(rawText, req.headers, secret?, tolerance?) / verifySignature(...) — use express.text({type:"application/json"}) or express.raw, not express.json() examples/openai/webhooks/verify_manual.ts
Ruby client.webhooks.unwrap(request.body, headers) → OpenAI::Models::Webhooks::ResponseCompletedWebhookEvent … —
Rust / PHP standardwebhooks reference libraries (Webhook::new(secret).verify(payload, headers)) —

Runnable proof (2026-09-18): python examples/openai/webhooks/offline_test.py and node examples/openai/webhooks/offline_test.ts — each signs a payload with a fake whsec_ secret and passes 11 checks (SDK unwrap, manual verify, tampered body, wrong secret, stale timestamp ±300 s, rotated double signature, missing header). Servers: server_express.ts, server_fastapi.py (UNVERIFIED as running processes — express/fastapi not installed here).

# 5. Operational checklist

  1. Create the endpoint via API to capture signing_secret programmatically; store it in a secret manager as OPENAI_WEBHOOK_SECRET.
  2. Verify → dedupe on webhook-id → enqueue → 200 immediately.
  3. Rotate with keep_old_secret_active_for_24_hours: true, deploy the new secret, then let the old one lapse; during the window the header carries two v1, signatures.
  4. Alert on status_code from /test, and on missing response.* events (webhooks complement, not replace, polling GET /v1/responses/{id}).
  5. Errors: bad/missing signature → respond 400 (never 2xx), so a tampered delivery is retried rather than silently dropped.