Transfers and endings
Two things that look unrelated and are the same subject: how a call stops being
this agent's problem. It is handed to a person, or it is over — and in both cases what your code
reads afterwards is end_reason.
Transferring to a person
Set transfer_number on the agent. That is the whole configuration, and it does two
things at once: it gives the agent the transfer_call tool, and it fixes where that
tool can dial.
/v1/agents/{id}curl -s -X PATCH https://voice.sphoro.com/v1/agents/$AGENT_ID \
-H "Authorization: Bearer $SPHORO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"transfer_number": "+912261234567"}'Tell the agent when to use it, in the prompt. Without instruction it will transfer either never or constantly:
Offer to put the caller through to a person if they ask for one, if they
are upset, or if they ask something about their account that you cannot
answer. Say who you are putting them through to before you do it.Forcing a transfer at a point in the conversation
On a flow, a node's tool field forces the call — a
transfer node that must transfer, rather than a prompt that suggests it.
{ "id": "handover", "prompt": "Tell the caller you are putting them through to the accounts team now.", "tool": "transfer_call" }What a transfer is, and is not
| What happens | |
|---|---|
| Cold transfer supported | The caller is
handed to the number. The agent leaves. The carrier tears our media stream down, which is why the
call records carrier_disconnected. |
| Warm transfer not yet | All three on the line while the agent introduces the caller. Not available — see numbers and carriers. |
call.transferred fires when it happens, carrying to. Only Twilio,
Plivo and Vobiz can transfer a live call.
Ending a call
Four ways, and all four write an end_reason.
| Who | How | Records |
|---|---|---|
| The caller | Hangs up | Whatever the carrier reports |
| The agent | Calls end_call, giving a reason in its own
words | That sentence, verbatim |
| You | POST /v1/calls/{id}/end | ended_by_api |
| A limit | max_call_seconds, or silence |
max_duration or no_input |
/v1/calls/{id}/endcurl -s -X POST https://voice.sphoro.com/v1/calls/$CALL_ID/end \
-H "Authorization: Bearer $SPHORO_API_KEY"You can also describe in words when the agent should hang up, with
conversation.hangup_prompt — at the cost of a model call per turn. See
conversation behaviour.
Every end reason
end_reason, not
status. A call that reached voicemail and a call that reached your customer
both have status: "completed". Code that treats completed as success
counts every voicemail as a conversation.Reached a conversation
The call happened. Whatever the outcome, do not retry these.
| Value | Means |
|---|---|
ended_by_api | You ended it with POST /v1/calls/{id}/end. |
max_duration | The call reached the agent's conversation.max_call_seconds. The agent said a short goodbye first. |
no_input | The caller went quiet. A conversation agent asks once whether they are still there and, hearing nothing, says goodbye; a keypad menu re-reads its options and then gives up. |
interrupt | The caller started talking over the agent, and it stopped. |
carrier_disconnected | The line dropped mid-conversation. Also how a transferred call ends, since the carrier tears our media stream down once the caller is with a person. |
dropped | A connected leg stopped sending without hanging up. |
announcement | A normal agent with no transfer number read its greeting out and hung up. That is what an announcement line is. |
Never reached a person — worth retrying
The call was placed and nobody talked to it.
| Value | Means |
|---|---|
no_answer | Rang out. |
unanswered | It rang and nobody picked up. On a console call, the carrier reported ringing and the ring window then ran out. |
busy | The line was busy. |
voicemail | An answering machine picked up rather than a person. |
not_connected | It never rang. The number was dialled and the far end's telephone never started, so the attempt never left the carrier. A different thing from ringing out, and worth a different response — repeated not_connected usually means a route problem, not an unavailable customer. |
at_capacity | Your account was at its concurrent-call ceiling. Nothing was dialled. Retry it. |
caller_gave_up | The caller hung up while it was still ringing. |
missed | It rang on a line a person answers and nobody took it. |
Do not retry
Retrying these produces the same answer and, in two cases, a compliance problem.
| Value | Means |
|---|---|
bad_number | There is no such number to reach. |
rejected | The far end refused the call outright. |
declined | An operator refused it while it rang. |
canceled | Cancelled before it connected. |
carrier_failure | The carrier could not place it — bad number, blocked route, no credit. |
over_budget | The project this agent bills to was over its monthly budget, so the call was refused with the busy line. Fix the budget, not the call. |
transfer_failed | A normal agent could not hand the call over. It has no model to apologise with, so the call ends rather than holding the caller on a line nobody is coming to answer. |
error | Something went wrong, reported in the same shape as an HTTP error. |
And one that is a sentence
When the agent ends the call itself, end_reason is the model's own reason from the
end_call tool — "the caller confirmed the appointment and had no other questions".
So treat the field as an open string with a well-known set of values, not a closed enum: a
switch with no default will meet one of these.
Where the statuses fit
status | Means |
|---|---|
queued | Accepted, not yet dialled. Every outbound call starts here. |
ringing | Ringing on a line where a person may answer. |
in_progress | Connected and talking. A browser call starts here, since there is nothing to dial. |
completed | Finished. Read end_reason to know
how. |
failed | Never became a conversation. |
Where the call came from
Every call also records who placed it, which is what you want when a number appears in a report and nobody admits to dialling it.
source | Means |
|---|---|
api | A direct request whose User-Agent named no client we
recognise. Your own integration, or curl. |
sdk | One of the published clients, or a browser token minted at
POST /v1/realtime/tokens. source_client names which, and at what
version. |
app | Placed on one of our own surfaces by a signed-in person — the console, the dialler, a test call from an agent page. |
campaign | The campaign dialler working through a list. Nobody asked for this call on its own. |
inbound | A carrier delivered it. Nothing on this side placed it, and
source_client names the carrier. |