Best practices
Debugging a voice agent
A method for locating a failure before you start changing things: which layer broke, what evidence exists, and where to look for it.
Debugging a voice agent is unusual in one specific way: the bug report is a story. Someone says "it hung up on me", or "it gave the wrong price", or "the transfer didn't work". None of those sentences names a system. The same account fits a routing problem, a prompt problem, a slow endpoint of yours, and a webhook you never received.
So the first move is never a fix. It is locating the failure in one layer, which usually takes minutes and saves a day of editing prompts that were never the problem.
Four layers, four kinds of evidence#
Every voice failure sits in one of these. Identify the layer, and the right troubleshooting page follows from it.
| Layer | The question it answers | Evidence | Where it is worked through |
|---|---|---|---|
| Reach | Did the call arrive, and did an assistant answer it? | Whether a call record exists at all | Calls and assistant behavior |
| Conversation | Did the assistant say and do the right things? | The transcript and message log | Calls and assistant behavior |
| Integration | Did your endpoints hold up their end? | Your own server logs, plus the tool turns in the transcript | Tool calls to your server |
| Delivery | Did the result reach your systems? | Your webhook handler's logs | Webhooks not arriving |
A fifth category sits outside the call entirely: your requests to the API being rejected. If you are debugging a 401, 403, or a 404 on a resource you are sure exists, that is Authentication failures, and the call layers above are a distraction.
Work backwards from the outcome#
The instinct is to start at the beginning of the call and read forward. Start at the end instead: the outcome you can observe narrows the layer fastest.
Is there a call record?#
List recent calls and look for the one in question.
curl "https://core-api.heysadie.ai/calls?limit=5" \
-H "Authorization: ApiKey YOUR_API_KEY"No record at all means the call never reached the platform. The problem is upstream at the number or the carrier, not in anything you have configured on the assistant. A record with a failure status means it arrived and something went wrong after that. This one question splits the Reach layer from everything else, and it costs one request.
Read the transcript, not the report#
The caller's account of a call is a summary of a summary. The transcript is what was said. Pull the full record:
curl https://core-api.heysadie.ai/calls/CALL_ID \
-H "Authorization: ApiKey YOUR_API_KEY"Read transcript and messages before forming a theory. A striking share of "the assistant did the wrong thing" reports turn out to be the assistant doing exactly what it was told, in response to something the caller said that nobody relayed to you.
Decide whether it is one call or a pattern#
One bad call and a systematic fault need completely different responses, and it is easy to spend a day fixing a coincidence. Compare against the population before you commit:
GET /calls/summary-by-assistantgives per-assistant call counts, average duration, and success rate.GET /calls/statusesreturns the status values that actually occur in your history. Build filters from it rather than guessing at values.GET /calls?status=…&dateFrom=…isolates the same shape of failure across a window.GET /calls/exportpulls up to 10,000 lean rows for offline analysis. It omits transcripts and recordings by design, so use it to find the calls worth reading, not to read them.
Reproduce it on a call you control#
Browser calling connects you to the same assistant that answers the phone number, without dialling. It accepts a per-call firstMessage, so you can open directly on the turn you are investigating instead of walking through the whole conversation each time.
If the failure only happens on real calls and never on yours, that difference is the clue: background noise, accents, speakerphone, and callers who interrupt are all things a clean test call does not reproduce.
Change one thing, then place another call#
Configuration is read when a call starts. The call in progress keeps what it began with, so every change is verified by a new call, and a new call verifies nothing if you changed three things before placing it.
Things that look like something they are not#
Most wasted debugging time goes into one of these five substitutions. Each has a cheap test.
"The model ignored my instruction." More often a different surface is supplying the behavior. developerPrompt, a shared prompt, up to six custom instructions, and linked task groups all reach the same conversation and compose rather than replace. Custom instructions are the easiest to forget you added. Test: list every surface attached to the assistant before editing any of them. Writing prompts for voice maps which one carries what.
"The tool is broken." Two entirely different failures wear this description. If the transcript shows no tool turn, the tool was never selected. That is the tool's description, or the tool not being in toolIds at all. If the transcript shows the assistant calling it and then stumbling, the tool ran and your server is the suspect. Test: search the transcript for the tool turn first.
"The webhook is broken." Usually it went somewhere you are not watching. An assistant's serverUrl override silently wins over the fallback URL you configured, so a per-assistant override set months ago sends everything to an endpoint nobody is tailing. Test: log unconditionally at the very top of your handler, before verification and before parsing. If nothing appears, the request is not reaching you at all, which is a different problem from rejecting it. Webhooks not arriving covers both branches, and receiving webhooks on localhost gets a real delivery onto your own machine where you can read it byte for byte.
"The assistant hung up on the caller." Check the deliberate exits before assuming a crash: callTimeLimitSeconds caps call length, and idleMessages combined with idleMessageMaxSpokenCount end a call after a set number of unanswered silence prompts. Both look like a drop from the caller's side. Test: compare the call's duration against the configured limit.
"The number is dead." failoverMode: true forwards every call straight to the failover number without the assistant ever answering. It is a deliberate kill switch and it is very easy to leave on after an incident. Test: read the assistant and check the flag before anything else.
What the evidence actually contains#
Knowing which field answers which question saves a lot of re-reading.
| Question | Field | Where |
|---|---|---|
| What was said? | transcript, messages | GET /calls/{id}, end-of-call report |
| How did it end? | status, endedReason | GET /calls/{id} (ended_reason on the report) |
| How long did it run? | startedAt, endedAt, callDuration | GET /calls/{id} |
| Was a hand-off attempted? | transfer_reason_id | End-of-call report |
| Did anyone pick it up? | supervisor_answered | End-of-call report (supervisorAnswered on the call record) |
| What was the call about? | summary, analysis, category, category_list | Both |
| What did it sound like? | recordingUrl | GET /calls/{id}, when recordingEnabled was on |
Two notes on choosing between the two sources. The end-of-call report is the highest-fidelity record and arrives the moment the call ends. If you are polling the calls API to find out when a call finished, take the webhook instead. And when the complaint is about timing rather than content (talking over the caller, long pauses, a whisper landing on top of a greeting), the transcript will not show it. You need the recording, which means recordingEnabled has to have been on at the time. That is a decision you make before the incident, not after.
Transcripts do not depend on recording
Transcription, summaries and analysis are produced on every call regardless of the recording setting. Turning recordingEnabled off costs you the audio, not the written record.
When you cannot reproduce it#
Some failures only occur on real traffic. Two things help more than trying harder to reproduce them.
Make the pattern visible. Filter the calls list by status and time window, and read four or five transcripts rather than one. What one call presents as randomness, five calls usually present as a specific phrasing the prompt does not handle, or a specific caller behavior: interrupting, going quiet, giving two answers at once.
Instrument your own side properly. Log every tool request you receive with the call_id, its arguments, your latency, and your response. When you next read a transcript that stalls at a tool turn, you can line it up against your own log for that exact call instead of inferring.
Escalating with something useful#
When you have to hand a call to someone else (a colleague, or whoever supports your account), these five things turn a conversation into an investigation:
- The call id and the time, from the call record or the end-of-call report.
- The exact turn in the transcript where behavior diverged from what you expected (quoted, not described).
- What you expected instead, and which configuration field you believe should have produced it.
- Whether it is reproducible, and on which of your test calls.
- For an API failure: the
error.code, plusmetadata.timestampandmetadata.pathfrom the error envelope. Not a screenshot of the message: the code is the stable identifier.
Next steps#
- Calls and assistant behavior: symptom-by-symptom fixes for the Reach and Conversation layers.
- Webhooks not arriving: the Delivery layer, including signature verification.
- Authentication failures: for the rejections that happen before a request reaches any data.
- Production readiness: the checks that stop most of this happening on live traffic.