rotate() - the only call that returns it in
full - and store it as UNPOD_WEBHOOK_SECRET. Then verify each request with
parse_call_event.
Which requests are signed
“Yes” applies once your org has a secret: until then every request goes out unsigned, so
create it before you point anything at a server that verifies.
parse_call_event rejects
an unsigned request. Parse those with verify=False and trust only what the API returns,
as Recording and
Number Masking do.
Get the secret
rotate() creates a new version and invalidates the previous one, so a server still
verifying with the old key rejects every request until you give it the new one.
pushed_to_sbc is true when the new key is already in use for signing. When it’s false,
Unpod keeps retrying the update in the background; until it lands, requests are still
signed with the previous key (or unsigned, for your first secret). See
The webhook secret for the cURL call.
Verify and parse
- Pass the raw body exactly as received. Re-encoding it breaks the signature.
Content-Typedecides form or JSON;parse_call_eventreads both.- The same event can arrive more than once (see
Delivery and retries). De-duplicate on
ev.event_id, and pass anidempotency_keywhen an event triggersadd_participant. - Return 2xx for events you don’t need, so they aren’t retried.
tolerance(default 300 s) rejects replays.verify=Falseskips the check: use it only for local tests and for the unsigned requests.
X-Webhook-Signature: t=<unix ts>,v1=<hex HMAC-SHA256(secret, "<ts>.<raw body>")>.
Delivery and retries
Unpod POSTs each status callback and recording event and waits up to 10 s for a 2xx. Any other status, a timeout or a connection error is a failure, and Unpod tries again: 4 attempts in all, about 1 s, 5 s and 30 s apart (each wait varies by up to 25%, so 27-45 s of waiting in all). Each attempt can take up to 10 s, so the window from the first attempt to the last is roughly 27-75 s. Then the event is marked failed and never sent again.- Events for one leg arrive in order (
ev.sequence). While one is being retried, that leg’s later events wait behind it; other legs aren’t held up. - An event queued before you call
rotate()is retried signed with the old key, so a server that already has the new key rejects it. - Return 2xx fast and do slow work afterwards. A reply slower than 10 s is a failure.
- Make handlers idempotent: the same
ev.event_idcan arrive twice, for example when your 2xx is lost. - A lost
AnsweredorHangupcan leave someone on the line. As a backstop, sweep your active calls withcalls.list()every few minutes, walking every page until one comes back empty, and end any that should be over. - Answer-URL requests aren’t retried: a failed
StartAppapplies the fallback, andHangupandRecordStopare sent once.
Events
CallEvent fields
Form wire names are below; with status_callback_format="json" the same fields come in
snake_case (call_id, leg_id, …).
Hangup causes
The strings are exact. Match on
ev.hangup_cause first. ev.sip_code is always set for
Busy Line, No Answer, Invalid Destination Address, Forbidden and
Service Unavailable. It’s None on an answered leg, and can be None for Rejected,
Cancelled, Unknown and Ring Timeout Reached. A ring timeout also reports the 487
from cancelling the ringing leg, so 487 alone doesn’t mean a cancel.