How webhooks carry call events to your systems
Webhooks carry call events by sending an HTTP request from the calling platform to an endpoint you control when something meaningful happens, such as a call starting, ending, transferring, or producing a final disposition. A reliable receiver authenticates the request, records its event identifier, returns a successful HTTP response quickly, and processes the event asynchronously. Because delivery can be retried, delayed, or reordered, webhooks should be treated as an at-least-once event channel rather than a remote procedure call or a live audio stream.
The event plane of a call
A programmable call usually has three related but distinct paths:
- The media plane carries RTP or another real-time audio stream.
- The control plane establishes, routes, modifies, and ends the call.
- The event plane reports what happened to systems that do not participate directly in signaling or media.
A webhook is part of that event plane. It can tell a CRM that a call completed, trigger post-call processing, update a dashboard, or report that a transfer failed. It normally does not carry continuous audio, and it should not be used as though it were a bidirectional media channel.
This separation is useful. The phone conversation can continue even if a reporting system takes time to update. It also introduces consistency work: the call platform and the downstream application no longer commit state in one shared transaction.
What is inside a call event
Implementations use different schemas, but a durable event envelope usually needs the same categories of information:
{
"event_id": "unique delivery-independent identifier",
"event_type": "call.completed",
"occurred_at": "event time",
"schema_version": "version understood by the receiver",
"call_id": "stable call identifier",
"data": {
"direction": "inbound",
"status": "completed"
}
}
The event identifier distinguishes one logical event from another. The call identifier groups events from the same call. They are not interchangeable: a single call can emit many events, and the same event can be delivered more than once.
An occurrence time describes when the event happened at the source. A receipt time describes when your endpoint saw it. Store both. If a retry arrives later, receipt order may not match call order.
Event-specific data might include routing results, timestamps, a call disposition, recording or transcript references, tool outcomes, or failure details. Consumers should use fields promised by the schema rather than inferring state from an event name alone.
The delivery sequence
For a non-blocking event, the normal exchange is short:
Call platform Your HTTPS endpoint Worker
| POST signed event | |
|--------------------------->| |
| | verify signature |
| | insert event if new |
| | enqueue durable job |
| 2xx response | |
|<---------------------------| |
| | process business action |
| |-------------------------->|
The receiver should finish only the work needed to accept the event safely. Signature verification, deduplication, durable storage, and enqueueing are appropriate in the request path. Fetching a full transcript, updating several business systems, or running analysis usually belongs in a worker.
This design shortens the period in which the sender is waiting. It also prevents a slow CRM or database query from causing a delivery retry even though your application already began the work.
HTTP status codes form a small delivery protocol. A successful 2xx generally means the event has been accepted. A timeout or server-side failure generally invites a retry. A client-error response may mean the request can never succeed as sent, although exact retry rules are platform-specific. Document the sender's behavior and test it rather than assuming all webhook providers interpret every status code identically.
Authenticate the raw request
An internet-accessible endpoint should not trust a JSON body just because its fields look plausible. The sender and receiver can share a secret and use an HMAC signature to authenticate the bytes of the request. HMAC is specified in RFC 2104; HMAC-SHA256 combines that construction with SHA-256.
Conceptually, the sender computes:
signature = HMAC-SHA256(endpoint_secret, bytes_defined_by_the_signing_contract)
The receiver computes the same value and compares it with the signature header using a constant-time comparison. The exact signed bytes are part of the API contract. Some schemes sign the raw body; others sign a timestamp plus a separator and the raw body. Follow the documented construction exactly.
Use the raw request body for verification. Parsing JSON and serializing it again can change whitespace, key order, or escaping while preserving the same data, producing a different byte sequence and an invalid signature.
A timestamp in the signing contract can limit replay. Verify that it falls within the receiver's accepted clock window, then still deduplicate by event identifier. The timestamp reduces reuse of an old captured request; the identifier prevents the same valid event from being processed twice.
Keep a separate secret per endpoint when the platform supports it. That limits the blast radius of a leaked credential and allows rotation without interrupting unrelated consumers. Secrets belong in a secret store, not source code or logs.
Network allowlists can add a layer, but they are not a substitute for message authentication. Source addresses can change, forwarding infrastructure can obscure them, and an allowed network does not prove which application created the body.
Assume at-least-once delivery
The sender cannot always know whether your system accepted an event. Consider this sequence:
- Your endpoint stores and enqueues the event.
- It sends
200 OK. - The connection breaks before the sender receives that response.
- The sender retries.
Both sides behaved reasonably, but your endpoint sees the same event twice. This is why “the sender retries only on failure” does not imply “duplicates happen only when the receiver failed.”
Make ingestion idempotent. A common pattern is a database table with a unique constraint on the event identifier. Insert the event and an outbox record in one database transaction, then publish the queued work separately. If the insert conflicts, return success for the already accepted event and avoid starting the business action again.
The worker also needs idempotency. A crash can occur after it updates a CRM but before it marks the job complete. Use an idempotency key in the target system where available, or store the external operation's state locally so a retry can determine whether the action already happened.
Deduplicating by call identifier is too coarse: it would discard legitimate later events from the same call. Hashing the whole payload is also fragile because retry metadata or representation can change. Prefer the source's stable event identifier.
Do not assume arrival order
Events can take different network paths and retry on different schedules. A call.completed event might arrive before an earlier progress event that was delayed. Two workers can also process accepted events concurrently.
Consumers should define ordering rules from source data, not delivery order. Options include:
- compare source sequence numbers when the schema provides them;
- compare state versions and apply only newer transitions;
- use occurrence times with explicit tie and clock-skew handling;
- model status transitions so a terminal state cannot be overwritten by a stale intermediate state;
- serialize work by call identifier when strict per-call ordering is required.
Do not make every consumer implement a complete call state machine unless it needs one. A billing exporter may process independent usage events, while a live operations panel may need carefully ordered status transitions.
Blocking webhooks are a different contract
Some call flows use a webhook synchronously to ask an application how to handle an incoming call. The request might supply the called number and caller context; the response might select an agent, reject the call, or provide call-specific configuration. That is closer to a remote decision point than an event notification.
The difference changes the engineering constraints:
- Latency is caller-facing. The call cannot progress until the response arrives or the platform falls back.
- The response is operational input. It needs strict schema validation and limited authority.
- Timeout behavior is part of routing. Decide whether to use a safe default, send the call to a fixed destination, or fail closed.
- Dependencies must be bounded. A slow chain of CRM and data-service calls can delay every inbound call.
Keep the synchronous path small. Cache stable routing data, set timeouts shorter than the platform's outer deadline, and define a default that is safe without the response. Send reporting and enrichment work to non-blocking events after the routing decision.
Connect events to business workflows
Call events are most useful when they carry a stable identity across systems. Map the call identifier to the relevant contact, case, campaign, or appointment, but do not assume the caller's phone number is a unique customer key. Numbers can be shared, forwarded, reassigned, or withheld.
Typical consumers include:
- a CRM integration that attaches the call and its outcome to a contact;
- a support workflow that opens or updates a case from a final summary;
- an analytics pipeline that measures a call flow from source events;
- a compliance process that applies retention and access rules to recordings and transcripts;
- an alerting service that notifies an operator about failed transfers or urgent dispositions.
Keep original event payloads in a restricted store long enough to debug delivery, subject to the data-retention policy for the call. Transform them into an internal event model before feeding many downstream systems. That adapter contains schema drift in one place.
Design for schema evolution
Webhook schemas change. New event types appear, optional fields are added, and an old field may eventually be replaced. A robust consumer should ignore unknown fields, validate required ones, and route unknown event types to inspection rather than crashing the endpoint.
Version the external contract and your internal transformation separately. If a breaking schema version arrives, quarantine it and alert. Do not accept a payload you cannot interpret and silently mark it processed.
When consuming a REST API after receiving an event, remember that the fetched resource may be newer than the event. Decide whether the workflow needs the event-time snapshot or the latest state. An event that says a call just ended and an API response fetched later are related, but they are not necessarily the same version of reality.
Observe and test the pipeline
At minimum, record the event identifier, event type, call identifier, receipt time, signature result, ingestion result, processing attempts, and final outcome. Do not log endpoint secrets or unrestricted sensitive payloads.
Track delivery age and queue age separately. A late source delivery and a backed-up worker both look like “the CRM updated late” to a user, but they require different fixes. Use a dead-letter path for events that exhaust internal processing attempts, with a controlled replay mechanism after the underlying issue is corrected.
Test valid delivery, invalid signatures, stale timestamps when used, malformed bodies, unknown types, duplicate events, out-of-order events, handler timeouts, dependency failures, and worker crashes after a side effect. Include webhook cases in the broader guide to testing voice agents.
On ThunderPhone
ThunderPhone documents multiple webhook endpoints with separate secrets and event subscriptions, blocking incoming-call configuration events, and non-blocking event delivery with exponential-backoff retries and HMAC-SHA256 signatures. A legacy single-URL organization webhook remains available for backward compatibility.
FAQ
Is a webhook the same as a function call during a conversation?
No. A webhook usually notifies your system about an event or requests call-time configuration. A conversational tool invocation lets the agent request a specific action during a turn. See how function calling works on live calls for that path.
Why should the endpoint respond before finishing the business workflow?
A quick acknowledgment separates delivery from processing. Slow external work then cannot make the sender assume delivery failed and retry an event your system already accepted.
Can HTTPS replace webhook signatures?
No. HTTPS protects data in transit and authenticates the server to the client. A valid HMAC signature lets your receiver verify that a party holding the endpoint secret created the signed request bytes.
What should a receiver do with a duplicate event?
Recognize it by its stable event identifier, avoid repeating downstream side effects, and normally return success because the logical event was already accepted.