sphoroVOICEdocs
Markdown

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.

PATCH/v1/agents/{id}
Shell
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"}'
The model chooses when to transfer, never where. There is exactly one destination and it is the one you configured. A model cannot be talked into transferring a caller to a number a caller supplies, which is a social-engineering path this API simply does not have.

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.

JSON
{ "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 supportedThe 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 yetAll 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.

WhoHowRecords
The callerHangs upWhatever the carrier reports
The agentCalls end_call, giving a reason in its own wordsThat sentence, verbatim
YouPOST /v1/calls/{id}/endended_by_api
A limitmax_call_seconds, or silence max_duration or no_input
POST/v1/calls/{id}/end
Shell
curl -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

Branch on 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.

ValueMeans
ended_by_apiYou ended it with POST /v1/calls/{id}/end.
max_durationThe call reached the agent's conversation.max_call_seconds. The agent said a short goodbye first.
no_inputThe 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.
interruptThe caller started talking over the agent, and it stopped.
carrier_disconnectedThe 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.
droppedA connected leg stopped sending without hanging up.
announcementA 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.

ValueMeans
no_answerRang out.
unansweredIt rang and nobody picked up. On a console call, the carrier reported ringing and the ring window then ran out.
busyThe line was busy.
voicemailAn answering machine picked up rather than a person.
not_connectedIt 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_capacityYour account was at its concurrent-call ceiling. Nothing was dialled. Retry it.
caller_gave_upThe caller hung up while it was still ringing.
missedIt 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.

ValueMeans
bad_numberThere is no such number to reach.
rejectedThe far end refused the call outright.
declinedAn operator refused it while it rang.
canceledCancelled before it connected.
carrier_failureThe carrier could not place it — bad number, blocked route, no credit.
over_budgetThe 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_failedA 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.
errorSomething 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

statusMeans
queuedAccepted, not yet dialled. Every outbound call starts here.
ringingRinging on a line where a person may answer.
in_progressConnected and talking. A browser call starts here, since there is nothing to dial.
completedFinished. Read end_reason to know how.
failedNever 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.

sourceMeans
apiA direct request whose User-Agent named no client we recognise. Your own integration, or curl.
sdkOne of the published clients, or a browser token minted at POST /v1/realtime/tokens. source_client names which, and at what version.
appPlaced on one of our own surfaces by a signed-in person — the console, the dialler, a test call from an agent page.
campaignThe campaign dialler working through a list. Nobody asked for this call on its own.
inboundA carrier delivered it. Nothing on this side placed it, and source_client names the carrier.