Answer detection
What a call carries about who answered it, what happens when only music or tones are heard, and how to change it per agent.
A carrier reports a call answered the moment it starts carrying audio. That is true of a person picking up, and equally true of a ringback replacement tune, a hold-music loop, or a line that answers into silence. So "answered" on its own cannot tell you whether anybody was there.
Every answered call therefore carries two separate facts:
- its status, which says the transport answered, and
- its
answer_classification, which says what was heard afterwards.
What the classification says
| Value | Meaning |
|---|---|
human | a person was heard |
non_human_audio | continuous music or tones held the line and nothing a person did was ever heard |
silent | the line answered and nothing at all was heard |
unknown | detection ran and reached no conclusion within its window |
"" | no verdict was reported at all |
The last two are not the same thing, and the difference is worth keeping.
unknown means detection ran and could not say. The empty string means nothing
was reported: the call was placed before this field existed, the agent has
collection switched off, or the call never carried media.
reason_detail qualifies the classification and nothing else:
| Value | Appears with |
|---|---|
caller_tune | non_human_audio |
hold_music | non_human_audio |
no_audio | silent |
"" | every other classification |
It is not ended_reason. That field says why the call ended; this pair says
what was heard when it started. A call can carry both.
Where the fields appear
Both fields are on the call resource returned by GET /v1/calls and
GET /v1/calls/{call_id}, on each call in
call group results, and in the call.completed
webhook body. They are also on the call rows in your workspace.
{
"call_id": "call_...",
"status": "completed",
"answer_classification": "non_human_audio",
"reason_detail": "hold_music"
}That is a call that ran its course against hold music. Nothing about how it ended changes today, which is the subject of the next two sections.
What counts as a person
A person is heard when speech starts and then stops. Concretely, either a burst of speech-shaped audio that is followed by a pause, or a recognised word whose audio has come to a stop. The pause is the part that matters: it is what separates somebody saying "hello" from a recording that never breaks.
The reverse verdict is deliberately harder to reach. Skysay only calls audio
non-human when it runs continuously for several seconds with no pause in it, its
shape lacks the rhythm of speech, and no word is recognised in that window.
Anything short of all three is unknown.
That narrowness is on purpose, and it has a stated cost in both directions:
- A recorded announcement is treated as a person. Announcements and menu
prompts have pauses in them, so they read as
humanorunknownand are left alone. Recognising those is a separate problem and is not what this does. - Somebody talking over music that never stops is missed. Their words never land in a pause, so they are not counted, and the call is classified on the audio's shape alone.
Detection runs on the audio itself, in every language. It does not depend on a transcript being available, and it never stores audio: only the verdict, its score and the timings are recorded.
The ending it is paired with
ended_reason has a value, no_human_after_answer, for a call the platform
ended because only non-human audio was ever heard after pickup. A call that ends
that way carries answer_classification non_human_audio and the detail naming
which kind, so the reason says that nobody was there and the classification says
what was heard instead.
Nothing ends a call this way today. The value exists in the published vocabulary so that your integration can be ready for it, and the ending itself arrives with a later release. When it does, a person heard at any point before the call ends cancels it, and the agent greets once more, so a late pickup is not hung up on.
A call that ends this way is reported as not connected:
- it is excluded from a campaign's connect rate, and from talk time;
- its campaign disposition is
no_answer, because no person was reached, so the contact is dialled again under your campaign's existing backoff rather than being treated as a conversation that happened; - its
termination_causestaysconnected, because that field describes the transport and the transport genuinely did answer. The two are separate questions and the fields answer one each; - carrier minutes up to the point it ends are real and are billed as usual.
Choose what an agent collects
answer_detection.non_human_audio is set per agent, in the Answer detection
card of the agent editor or through
PATCH /v1/agents/{agent_id}/voice-stack.
| Value | What it does |
|---|---|
shadow | Records what was heard on every answered call and changes nothing the caller experiences. |
off | Collects nothing. Calls carry no answer classification at all. |
shadow is what an agent with no stored value gets. A third value that acts on
the verdict and ends the call arrives with the release that can perform it;
until then sending it is refused with 400, because a setting you could select
that did nothing would be worse than one that is not offered.
curl -X PATCH https://api.skysay.ai/v1/agents/agt_123/voice-stack \
-H "Authorization: Bearer $SKYSAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"answer_detection": {"non_human_audio": "off"}}'Choosing a value is not the same as leaving it alone
This setting deliberately behaves differently from the other per-agent tuning sections. Everywhere else, saving a value equal to the current default is the same as saving nothing. Here it is not:
- an absent
answer_detectionkey means "follow the platform default", which isshadowtoday and is expected to change once the detector has been measured; - an explicit value is stored as you sent it, including
shadow, and the agent keeps it when the platform default moves.
So if you want an agent to observe and never do anything else, send shadow
rather than leaving the section out. The editor makes the same distinction: the
first choice is Platform default, and Shadow beside it is the explicit one.
The setting applies when a call starts; saving it never changes a call already in progress.
shadow is the platform default on purpose. It is the only setting that
measures how often the verdict would have been wrong, on real calls in real
languages, while being unable to end any of them.