Concept
Webhooks & status callbacks
TelVox reaches your application in three ways: a synchronous answer-URL request that asks what to do with a live call, status callbacksthat report one call's progress, and event webhooks that fire on lifecycle events across your account. All of it travels over the same signed, SSRF-safe egress that powers Dial's integrations.
Payload field names and the signature header format below are illustrative and may change before general availability. The delivery engine itself is real and shipped: the nine lifecycle events, custom headers, JSON payload templates, retries with timeouts, encrypted secrets, and the per-organization host allow-list are all in production in Dial today.
answer_url vs status_callback
These look similar but do opposite jobs. The answer_url is synchronous and instructive: TelVox blocks on your response and runs the call-control document you return. A status_callback is asynchronous and informational: TelVox tells you a call changed state and expects only a quick acknowledgement — the call proceeds regardless of what you return.
| answer_url | status_callback | |
|---|---|---|
| Timing | Synchronous — TelVox waits. | Asynchronous — fire-and-forget. |
| Your reply | A call-control document. | A 2xx acknowledgement. |
| Effect | Drives the live call. | Records state; doesn't steer. |
Status callbacks
Attach a status_callback URL when you create a call to follow its lifecycle. TelVox POSTs as the call moves through initiated, ringing, answered and completed. Recording produces its own callback stream — in-progress/completed/absent/failed — covered below.
POST /voice/status HTTP/1.1
Host: your.app
Content-Type: application/json
TelVox-Signature: t=1718995200,v1=5d41402abc4b2a76b9719d911017c592...
{
"call_sid": "CA9f3a2b...",
"event": "answered",
"from": "+14155550100",
"to": "+14155550199",
"direction": "outbound",
"timestamp": "2026-06-22T18:01:14Z"
}// Acknowledge quickly. A 2xx means "received" — do slow work
// (DB writes, downstream calls) on a queue, not inline.
HTTP/1.1 200 OKRecording-status callbacks
A separate callback reports the state of a recording, so you know when a signed-URL fetch will succeed.
| RecordingStatus | Meaning |
|---|---|
| in-progress | Recording has started and is actively capturing. |
| completed | Recording finished and is available for retrieval. |
| absent | No recording was produced (e.g. nothing was captured). |
| failed | Recording could not be completed. |
Event webhooks
Where status callbacks track a single call you started, event webhooks notify you of lifecycle events across your account — independent of any one request. You subscribe an endpoint to the event types you care about; each subscription can carry custom headers and a JSON payload template. TelVox supports nine lifecycle events today.
| Event | Fires when |
|---|---|
| web-form | A web form or inbound lead submission was captured. |
| call-start | A call connected and began. |
| hangup | A call ended; carries the final disposition timing. |
| no-agent | No agent was available to take a routed call. |
| disposition | A call was wrapped with an outcome/disposition code. |
| callback | A scheduled callback came due or was requested. |
| queued | A call entered an ACD queue to wait for an agent. |
| abandoned | A waiting caller hung up before being answered. |
| voicemail-left | A caller left a voicemail; links the recording. |
POST /telvox/events HTTP/1.1
Host: your.app
Content-Type: application/json
TelVox-Signature: t=1718995200,v1=a3f1...
X-My-Custom-Header: configured-per-subscription
{
"event": "voicemail-left",
"id": "EVb71c...",
"occurred_at": "2026-06-22T18:09:40Z",
"data": {
"call_sid": "CA9f3a2b...",
"recording_sid": "RE5e22...",
"duration": 23
}
}Signed & SSRF-safe delivery
Every delivery is signed and every target is checked against a per-organization host allow-list before TelVox connects to it.
- Signatures. Each request carries a
TelVox-Signatureheader — an HMAC over the timestamp and raw body, keyed by your subscription secret (stored encrypted at rest). Verify it before trusting a payload. - SSRF-safe egress.Outbound delivery is restricted to an allow-list of hosts you configure; requests to internal, private or link-local addresses are blocked, so a misconfigured URL can't be turned into a server-side request forgery vector.
- Replay protection. The signed timestamp lets you reject stale deliveries — drop anything outside a short window (a few minutes) even if the signature is otherwise valid.
Verifying a signature
Recompute the HMAC over the timestamp and the raw request body (not a re-serialized object — re-ordering keys breaks the hash), then compare in constant time. Reject deliveries whose timestamp is too old.
import crypto from "node:crypto";
// Recompute the signature over the raw body + timestamp and
// compare in constant time. Reject anything older than ~5 min.
export function verify(rawBody, header, secret) {
const { t, v1 } = parseHeader(header); // "t=...,v1=..."
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(v1),
);
}Retries, timeouts & idempotency
TelVox treats a fast 2xxas success. If your endpoint times out or returns a non-2xx, delivery is retried with backoff up to a per-subscription limit; deliveries are rate-limited so a burst of events can't overrun your endpoint. Acknowledge first and do the slow work on a queue — keep the handler fast so it never times out.
- Make handlers idempotent — key off the event
idso a retried delivery is processed once. - Return 2xx as soon as you've durably received the event; defer downstream work to a background job.
- Events can arrive out of order or more than once under retry — design for at-least-once delivery.
Next: Webhooks reference · Calls reference