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 v1. 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. It is 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, since 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, so design for at-least-once delivery.
Next: Webhooks reference · Calls reference