Set up webhooks
We send one POST per completed record to a URL you configure. There is one event, cdr.completed, and it carries the record.
Point us at your endpoint
Go to Organization → Settings, put your URL in Webhook URL, and save. It has to be reachable from the public internet. There is no tunnel for local development, so use a request-inspection service or a deployed staging endpoint while you build. Clearing the field switches webhooks off.
The URL is only checked for being a URL. A typo, a private hostname, or a stale host all save cleanly and then fail at delivery time with nothing in the dashboard to tell you, so confirm your first record arrives rather than assuming.
Then generate a signing secret in the same section. It is optional and you should do it anyway: without one, anyone who learns your URL can post whatever they like to it, and you have no way to tell. Verifying signatures is one function.
There is no test-event button. Capture a CDR on a real page and watch what arrives. Setting the URL does not backfill either. Records captured while it was blank are never delivered, and enabling webhooks starts at your next capture.
What arrives
A POST with a JSON body and these headers:
Content-Type: application/json
X-EC-Webhook-Id: 6f9619ff-8b86-4fd1-b42f-7fd68a2c0f5a
X-EC-Timestamp: 1741234571
X-EC-Signature: sha256=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08X-EC-Signature is present only when you have set a signing secret. The body is on the cdr.completed event.
Answer with any 2xx. We read nothing out of a successful response, so acknowledge as soon as you have the payload stored and do your own work afterwards. The request is abandoned after 15 seconds and treated as a failure. If you answer 4xx or 5xx we read the first kilobyte of your body and log part of it so we can help you debug, so keep credentials out of your error responses.
Nothing caps the size of what we send. A record carrying the maximum metadata and fifty long disclosures runs to a few hundred kilobytes, and an endpoint behind a small request-body limit answers 413, which is a permanent failure. Size your limit generously.
When it does not get through
Anything other than a 2xx is a failed delivery, and what happens next depends on which kind.
Retried: a 5xx, a 429, a connection error, and a timeout. Five attempts in total, so four retries, each waiting roughly twice as long as the one before starting at 30 seconds. The last one lands about eight minutes after the first. There is no hour-long queue: if your endpoint is down for a quarter of an hour, the record has left the webhook pipeline.
Those five are the queue's budget rather than five requests to your endpoint. An attempt can be spent before the POST goes out, so a failing endpoint may see fewer.
Not retried: anything below 500 other than 429. A 400, 401, 403, or 404 says the request itself is wrong and an identical one will not fare better. We follow redirects, so a 301 to another host is fine and the status at the end of the chain is what counts.
We do not deliberately re-send a record we have recorded as delivered. There is
no replay button in the dashboard, and no endpoint asks for one. So if you put
authentication in front of your endpoint and forget to let us through, every
record captured before you notice arrives as a 401, is never retried, and is
gone from your pipeline eight minutes later. Reconcile against the API rather
than assuming.
To reconcile, list your records and read webhookSuccessfullySentAt. It is set only when your endpoint answered 2xx, and its absence means no delivery succeeded. A metadata filter on your own identifier is how you find what you missed. See CDR endpoints.
Delivery guarantees, stated plainly
At least once, not exactly once. We skip a record whose delivery we have already recorded as successful, but that record is written after your response arrives. Anything failing in between (your 2xx lost on the way back, or our own write not landing) produces a second delivery of a record you already processed. Make your handler idempotent.
webhookId and the X-EC-Webhook-Id header identify the attempt, not the
record. Every retry carries a new one, so storing them and skipping repeats
will not stop you processing the same record twice. cdrId is the stable key.
The download URL
The payload's downloadUrl is good for seven days, rather than the 300 seconds a URL fetched from the API lasts. That is deliberate: a webhook consumer is often a queue worker that will not reach the record for a while.
Seven days is still not storage. Store the cdrId and ask the API for a fresh URL when you need one.
It is present only when your organization can already download the record, which for captures on your own domains means billing is set up and auto-collect is on. An organization with no billing receives every event and never receives a downloadUrl. If it is missing and you expected it, access and collection explains why.
Records we have flagged
A record we have marked invalid (bot traffic, most often) is still delivered, with invalidated: true and no downloadUrl. You are told it exists precisely so you know not to use or sell it.
Only the download is withheld. The IP address, the user agent, your metadata, and the derived consent facts all still arrive, so a record you must not act on is still consumer data you have received and now have to look after.