When webhooks fire
A webhook is sent once per request, when its state becomes one of:
No webhook is sent while a request is still
PROCESSING.
Configuration
You can set your webhook endpoint in two places:Account default
Ask your account team to set an account-level webhook URL. Once configured, this becomes the default for every request you create. No per-requestwebhookUrl field needed.
Per-request override
Pass awebhookUrl field in the body of POST /v1/requests to direct a single request to a different URL. The per-request value always wins over the account default.
GET /v1/requests/{requestId} for results.
Payload
The webhook body is intentionally compact. To get the full result (
results, missingFields, modality, data_completeness, and the error object on non-SUCCESS), call GET /v1/requests/{requestId} after receiving the webhook.
By the time you receive the webhook, the request is fully readable. For phone-backed requests, the call enrichment fields (
transcript, recordingDownloadUrl, callDuration, callSummary) are available on the GET /v1/requests/{requestId} response.Signature verification
Every webhook delivery includes anX-Webhook-Signature header. Verify it on every delivery. Without verification, anyone who learns your endpoint URL can forge events.
The signature is HMAC-SHA256 of the raw request body. The signing secret is your account’s webhook secret if you’ve set one up on the portal, otherwise it is your production API key. The same production-side secret is used for sandbox webhooks too. Your sandbox API key will fail verification. Compute the HMAC over the raw body bytes exactly as received: do not parse and re-serialize the JSON, since that changes the byte sequence and breaks the signature.
The header value is the bare 64-character lowercase hex digest, with no sha256= prefix or other framing. Example:
Python
Node.js
hmac.compare_digest / crypto.timingSafeEqual); use the equivalent in your language rather than == to avoid timing-attack risk.
The signature doesn’t include a timestamp, so it can’t be used to detect replays on its own. Use requestId as a dedup key (see Idempotency below).
Delivery and retries
We send the webhook when the request reaches a terminal state. On a 5xx response or connection failure we automatically retry up to 3 more times (4 attempts total) with exponential backoff (0.5s → 1s → 2s). A 4xx response is treated as terminal: we don’t retry, since re-posting the same payload is unlikely to succeed. If all four attempts fail, the failure is recorded and no further attempts are made. Fall back to pollingGET /v1/requests/{requestId} to recover.
For high-reliability ingestion:
- Ack with 2xx as soon as you’ve durably enqueued the event for processing.
- Track the
requestIds you’ve submitted on your side. Any request whose terminal outcome you haven’t received within the expected window should trigger a fallback poll.
Idempotency
In rare cases the samerequestId may arrive more than once. Always treat requestId as a dedup key:
internalId when creating the request, you have two layers of dedup keys. Pick whichever fits your system.
Recommended response
Return HTTP 200 as quickly as possible. Don’t do synchronous work that could exceed our 10-second timeout. Push processing to a background job and ack immediately. 5xx responses and connection failures trigger automatic retries; 4xx responses don’t (see Delivery and retries above). If all retries are exhausted, fall back to pollingGET /v1/requests/{requestId} to recover.
After acking, call GET /v1/requests/{requestId} to fetch the full payload (results, missingFields, modality, and the error object on FAILURE).