# 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**

```bash
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](https://voice.sphoro.com/docs/flows), a node's `tool` field forces the call — a transfer node that must transfer, rather than a prompt that suggests it.

**JSON**

```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** 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](https://voice.sphoro.com/docs/telephony). |

`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` |

`POST /v1/calls/{id}/end`

**Shell**

```bash
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](https://voice.sphoro.com/docs/behaviours).

## 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.

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