Debugging Twilio Voice Webhooks: A Step-by-Step Guide
Marcus has an IVR that intermittently routes calls to the wrong queue. He knows a Twilio webhook is involved. He's looking at 400 events from 80 different calls, all interleaved. This is the debugging workflow that gets from symptom to root cause in under 20 minutes.
Marcus works at a company that uses Twilio for customer support call routing. His IVR is intermittently routing calls to the wrong queue. The symptom is reproducible — roughly one call in forty ends up in the wrong queue, and only during business hours, which is suspicious. The cause is not reproducible in staging. He knows a Twilio webhook is involved, specifically the status callback that fires when a call is transferred between queues. But he is looking at a raw webhook log with 400 events from 80 different calls and 12 different event types, all interleaved by timestamp, and the failing calls are not obviously distinguishable from the successful ones except that they went wrong.
This is not an uncommon scenario. Twilio voice webhooks are designed for flexibility but not for debuggability. A single call generates between 5 and 15 webhook events depending on what happens. All of those events arrive at the same endpoint. The only way to group them is the CallSid field. The only way to spot a missing event is to count the expected events for each call and notice when the count is wrong.
If you are debugging Twilio voice webhooks manually, you are doing a lot of mental bookkeeping that should be done by a tool.
The Twilio Voice Webhook Lifecycle
Understanding what events a call generates — and in what order — is the foundation of debugging. There is no "standard" call because TwiML verbs, queue configurations, recording settings, and transfer logic all affect which events fire. The Twilio webhook documentation covers the full event reference. But a representative outbound call with recording enabled looks like this:
initiated— the call was created in Twilio's system. Your outbound dial was accepted.ringing— the destination phone is ringing.in-progress— the call was answered.recording-status-callback— fires within-progresswhen recording starts.recording-status-callback— fires again withcompletedwhen the recording ends.completed— the call ended. Duration, direction, cost, and final status are included.
For an inbound call to an IVR with a queue transfer, the sequence expands:
in-progress(inbound call answered by IVR)enqueuestatus callback — call entered the queuewait-urlcallbacks — periodic callbacks while the caller waits on holddequeue/ transfer — call was transferred to an agentcompleted— call ended
The optional events are where most debugging problems live. recording-status-callback requires a configured recordingStatusCallback URL in your TwiML. transcription-ready requires transcription to be enabled. Status callbacks for queue events require explicit configuration in both TwiML and the Twilio console. If any of these configuration points is missing or wrong, the event silently does not fire.
The Correlation Problem
Every one of these events arrives at a single endpoint. A production webhook handler might receive events from dozens of concurrent calls, mixed together in chronological order. The CallSid field is present in every event and is the only reliable correlation key.
A raw log entry for a completed call looks like this:
POST /webhooks/twilio 11:42:03 CallSid=CAabc123 CallStatus=initiated
POST /webhooks/twilio 11:42:04 CallSid=CAdef456 CallStatus=initiated
POST /webhooks/twilio 11:42:07 CallSid=CAabc123 CallStatus=ringing
POST /webhooks/twilio 11:42:08 CallSid=CAdef456 CallStatus=ringing
POST /webhooks/twilio 11:42:14 CallSid=CAabc123 CallStatus=in-progress
POST /webhooks/twilio 11:42:17 CallSid=CAdef456 CallStatus=answered
POST /webhooks/twilio 11:42:14 CallSid=CAabc123 RecordingStatus=in-progress
...
Manually debugging this requires filtering by CallSid, sorting by timestamp, and verifying the sequence. For 80 calls with 8 events each, that is 640 log lines to mentally organize. For Marcus's problem — one bad call in forty — it means comparing the event sequence of 39 successful calls to the sequence of the one that failed, looking for a difference.
This is technically possible. It takes a long time and is error-prone. A missing event does not announce itself. You have to notice its absence.
The Missing Event Problem
Missing events are the hardest Twilio debugging scenario because they are invisible in a raw log — there is no error, no 5xx, no indication that anything went wrong. See the webhook debugging checklist for a systematic approach to finding them. If recording-status-callback never fired, your log simply does not contain a recording status entry for that CallSid. There is no error. No 5xx. No indication that anything went wrong. You have to count expected events against actual events to notice the gap.
This is complicated by the fact that the expected event count varies by call. A call that was declined before answering has a shorter event sequence than a call that went to voicemail. A transferred call has events that a non-transferred call does not. You cannot simply check "did I get 8 events per call?" — you need to know what 8 events means for each specific call path.
Some specific scenarios that produce missing events:
Recording callbacks not configured. You added recording to your TwiML but forgot the recordingStatusCallback URL. Recordings complete and the recording-status event never arrives. You discover this when you try to retrieve a recording and it does not exist.
Transfer webhooks firing to the wrong URL. When you transfer a call to a queue, the callback URL for transfer events defaults to your account-level status callback URL — not the URL you configured for the original call. If your account-level status callback is misconfigured or different from your handler URL, transfer events go somewhere you are not watching.
Status callbacks for queue wait-url. The waitUrl verb fires against a URL you specify. If that URL returns content but your handler does not log the request (because it is a different handler), you lose visibility into those events.
Twilio signature verification rejecting valid events. This one is particularly difficult to debug. If your URL has a trailing slash in one place but not another, or if your handler is behind a proxy that modifies headers, Twilio's HMAC signature will not match. Your handler returns 403. From the outside, no event was processed. The event exists in Twilio's logs but not in yours.
HookTunnel Call Traces
HookTunnel's call trace feature auto-groups Twilio events by CallSid into a per-call timeline. Instead of a flat list of events sorted by arrival time, you see one row per call, expandable into a chronological sequence of events for that call only.
Missing events show as visible gaps in the timeline. If a call's sequence should be initiated → ringing → in-progress → recording-started → completed and the recording event is absent, the timeline shows initiated → ringing → in-progress → [gap] → completed. The gap is labeled with the expected event type based on the call context — HookTunnel infers expected events from the events that did arrive.
Each event in the timeline shows:
- Delivery status: did HookTunnel receive this event?
- HTTP status: what did your handler return?
- Processing status: if you have outcome receipts configured, did the outcome confirm?
- Payload: the full event payload, formatted and searchable
For Marcus's debugging session, the workflow is: open HookTunnel, navigate to Twilio call traces, filter by date range, sort by outcome (failing calls first). Click on a call that routed incorrectly. The timeline loads. He can see immediately whether the transfer status callback arrived, what his handler returned, and whether the receipt confirmed the queue assignment was applied.
Debugging Marcus's IVR
The timeline for Marcus's failing calls shows: initiated → ringing → in-progress → completed. The transfer event is missing entirely. It never arrived at Marcus's handler.
This narrows the problem significantly. The IVR is making the routing decision. The transfer is executing. But the callback confirming the transfer is not arriving. The issue is in the TwiML or the Twilio console configuration, not in the handler.
Marcus checks his TwiML for the transfer action. The <Enqueue> verb has a waitUrl configured. The action attribute — which fires when the call leaves the queue — is not set. That means when the call is transferred, Twilio uses the account-level status callback URL instead of Marcus's handler URL. The account-level URL is an old staging endpoint that no longer exists. Twilio sends the transfer callback there. The endpoint returns 404. No event reaches Marcus's production handler.
Without the per-call timeline, Marcus would have spent an afternoon grepping logs, comparing event sequences manually, and eventually escalating to Twilio support to pull server-side logs and trace what happened to that specific delivery.
With the timeline, the missing event is visible in 30 seconds. The root cause takes another 5 minutes to confirm.
Step-by-Step: Twilio Voice Webhook Setup
For completeness, here is the full setup for a reliable Twilio voice webhook handler.
Signature Verification
Twilio signs every webhook request with an HMAC-SHA256 signature. Verifying it requires the full URL that Twilio was configured to call — not your handler's path, but the complete URL including protocol, host, path, and query parameters:
import twilio from 'twilio';
function verifyTwilioSignature(req: Request): boolean {
const signature = req.headers['x-twilio-signature'] as string;
const authToken = process.env.TWILIO_AUTH_TOKEN!;
// CRITICAL: URL must match exactly what Twilio has configured
// Include query parameters if any — they are part of the signature
const url = `https://your-handler.com${req.originalUrl}`;
return twilio.validateRequest(authToken, signature, url, req.body);
}
The most common signature verification failure is a URL mismatch. If your Twilio console shows https://example.com/webhooks/twilio but your code assembles the URL with a trailing slash, verification fails on every request. If your Twilio console shows https://example.com/webhooks/twilio but your code assembles the URL as https://example.com/webhooks/twilio/ (trailing slash), verification fails on every request. If you are behind a load balancer that strips the port, or a proxy that rewrites the host header, the assembled URL will not match. Log the URL you are verifying and compare it to what is in your Twilio console when debugging.
Status Callback Handler
A handler that processes voice status callbacks:
app.post('/webhooks/twilio/voice', (req, res) => {
if (!verifyTwilioSignature(req)) {
return res.status(403).send('Forbidden');
}
const {
CallSid,
CallStatus,
Direction,
From,
To,
Duration,
RecordingUrl,
RecordingSid,
} = req.body;
// Route to appropriate handler by status
switch (CallStatus) {
case 'completed':
handleCallCompleted({ CallSid, Duration, Direction, From, To });
break;
case 'busy':
case 'failed':
case 'no-answer':
handleCallFailed({ CallSid, CallStatus, Direction, From, To });
break;
}
// Always return 200 quickly; process async
res.status(200).send('<Response/>');
});
Note the <Response/> — Twilio expects a TwiML response for voice callbacks, not a plain JSON body. An empty TwiML response is the correct way to acknowledge a status callback with no further instructions.
Recording Status Callback Handler
Recordings fire separate callbacks at different lifecycle points:
app.post('/webhooks/twilio/recording-status', (req, res) => {
if (!verifyTwilioSignature(req)) {
return res.status(403).send('Forbidden');
}
const { RecordingSid, RecordingStatus, RecordingUrl, CallSid, Duration } = req.body;
if (RecordingStatus === 'completed') {
// Recording is available for download at RecordingUrl
// Link to the call via CallSid
queueJob('process-recording', { RecordingSid, RecordingUrl, CallSid, Duration });
}
res.status(200).send('<Response/>');
});
Configure this URL in your TwiML's recordingStatusCallback attribute, not just in the Twilio console. The console setting is a fallback. TwiML-level configuration is per-call and takes precedence.
Configuration Checklist
Before going to production with a Twilio voice integration, verify each of these:
In the Twilio console:
- Voice webhook URL matches the full URL your handler is reachable at, with no trailing slash discrepancy
- Account-level status callback URL is set to your production handler (fallback for misconfigured TwiML)
- Geographic permissions include all countries you expect to call or receive calls from
In your TwiML:
- Every
<Dial>action with transfers includes an explicitactionURL - Every
<Enqueue>with dequeue logic includes an explicitactionURL - Recording callbacks are configured at the TwiML level, not just the console level
In your handler:
- Signature verification uses the exact URL Twilio has on file (test by temporarily logging mismatches)
- Response is
<Response/>for voice callbacks, not JSON - All processing after 200 is async
- CallSid is logged for every event (required for manual correlation if your tooling is unavailable)
The Difference Tooling Makes
Manual Twilio debugging is a skill, but it is also a time sink. The correlation problem — grouping events by CallSid and spotting missing ones — is mechanical work that scales with call volume. See HookTunnel's webhook inspection features for how call trace tooling eliminates this bookkeeping. For a broader framework on silent webhook failure, the same principles apply across providers. The correlation problem — grouping events by CallSid and spotting missing ones — is mechanical work that scales with call volume and gets harder as the problem gets more subtle.
Call trace tooling does not make you a better Twilio debugger. It makes the mechanical part disappear so you can focus on the part that requires judgment: understanding what a specific call sequence means, why a particular event should have fired but did not, and what in the IVR logic or configuration produced the outcome you are seeing.
Marcus's 40-minute investigation became a 5-minute one. Not because he became better at debugging Twilio — because he stopped doing the bookkeeping by hand.
Stop guessing. Start proving.
Generate a webhook URL in one click. No signup required.
Get started free →