Skip to content

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.

LayerThe question it answersEvidenceWhere it is worked through
ReachDid the call arrive, and did an assistant answer it?Whether a call record exists at allCalls and assistant behavior
ConversationDid the assistant say and do the right things?The transcript and message logCalls and assistant behavior
IntegrationDid your endpoints hold up their end?Your own server logs, plus the tool turns in the transcriptTool calls to your server
DeliveryDid the result reach your systems?Your webhook handler's logsWebhooks 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.

Terminal
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:

Terminal
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-assistant gives per-assistant call counts, average duration, and success rate.
  • GET /calls/statuses returns 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/export pulls 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.

QuestionFieldWhere
What was said?transcript, messagesGET /calls/{id}, end-of-call report
How did it end?status, endedReasonGET /calls/{id} (ended_reason on the report)
How long did it run?startedAt, endedAt, callDurationGET /calls/{id}
Was a hand-off attempted?transfer_reason_idEnd-of-call report
Did anyone pick it up?supervisor_answeredEnd-of-call report (supervisorAnswered on the call record)
What was the call about?summary, analysis, category, category_listBoth
What did it sound like?recordingUrlGET /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:

  1. The call id and the time, from the call record or the end-of-call report.
  2. The exact turn in the transcript where behavior diverged from what you expected (quoted, not described).
  3. What you expected instead, and which configuration field you believe should have produced it.
  4. Whether it is reproducible, and on which of your test calls.
  5. For an API failure: the error.code, plus metadata.timestamp and metadata.path from the error envelope. Not a screenshot of the message: the code is the stable identifier.

Next steps#