Carrier callback verification
Every carrier callback is verified against the provider's signature before any value in it is used, and every normalised event is stored once in call_events with its signature nonce under a partial unique index. That index is the replay protection: a webhook delivered twice writes one event. Verification runs inside the workspace that owns the number, because the credentials it needs are workspace-scoped.
What is checked, and in what order#
Read the provider's signature header and recompute it over the request as the adapter for that provider defines it.
Result The request is either genuine or it is not. Nothing in the body has been trusted yet.
Resolve the dialled number to the owning workspace and enter it.
Result The workspace-scoped signing credentials are now readable. Outside the workspace they read as empty, which is not the same as wrong.
Normalise the provider's event into Connect's own vocabulary and write it to
call_eventswith the signature nonce.Result A partial unique index on the nonce refuses a second write of the same delivery.
Act on the event — answer, speak, record an ending, close the row.
Result The action happens once per genuine delivery, no matter how many times the carrier sends it.
The adapter is the only layer that knows the provider's signature scheme. Everything above it sees a verified, normalised event and never a provider's own field names — a boundary that a test asserts rather than one that is merely intended.
The failure that made this a scoping problem#
_carrier_form originally verified the workspace's own number outside any workspace scope. The verification code was correct; the credentials it compared against were not there, because they are workspace-scoped rows and an unscoped read returns an empty result rather than an error. Every real call was rejected 403 before the caller heard anything at all.
Nothing about that failure looked like a security problem from inside Connect. It looked like a carrier that had stopped sending calls. The diagnosis came from the provider's own log, which showed the deliveries arriving and being refused. Verification now happens inside the workspace that owns the number, in the same movement as everything else the handler reads.
Replays, retries and the nonce index#
Carriers retry. A network blip, a proxy 502, a deploy restart mid-request — any of these produce a second delivery of an event that already happened. Storing every normalised event once, keyed by the signature nonce, means the second delivery is recognised as the same delivery and not as a second hangup, a second answer or a second charge.
| Situation | Treatment | Why |
|---|---|---|
| Same nonce, second delivery | Stored once; the duplicate is discarded | The partial unique index refuses it |
A request a call cannot lose (arrived, brief, end) | Retried through a restart or a proxy 502 | Losing one of these loses the call |
A 4xx from Connect | Never retried | A 4xx is an answer, not a transport failure |
| A dial the carrier answered with 408, 503 or 504 | Dialled once more | A timeout is not evidence the call happened |
| A leg answered at the carrier that never joins the room | Written off after the join timeout | Waiting indefinitely holds capacity for nothing |
The distinction in the third row is the one worth carrying away. Retrying on a 4xx turns a clear refusal into a loop, and on telephony a loop costs money in real time.
What a caller experiences when verification fails#
Nothing that helps them. A refused callback means Connect never answers, so the caller hears whatever the carrier does with an unanswered call — ringing, a network message, or a drop. There is no Call row, because the row is created after the request is believed.
That asymmetry is deliberate: an unverified request must not be able to create records or consume capacity. It also means the audit for this class of problem lives at the provider rather than in Connect. Reading the carrier's call log beside the calls list is the fastest way to see it, and a Completed on their side with no row on this one is a bug every time.
Questions#
Does Connect verify outbound webhooks it sends, as well as ones it receives?
Those are separate mechanisms. This page is about callbacks arriving from a telephony carrier. Signing on Connect's own outbound webhooks is described in Verifying a Connect webhook signature.
Why is the nonce index partial rather than a plain unique constraint?
Because not every normalised event carries a signature nonce — some are written by Connect itself rather than derived from a provider delivery. A partial index applies uniqueness exactly where the nonce exists, without forcing a synthetic value onto rows that have none.
If a carrier retries after Connect has already ended a call, what happens?
The duplicate is discarded by the index and the ending stands. This matters more than it sounds: an ending applied twice can overwrite an attribution and a cost, which is why the guard is at the storage layer rather than in each handler.