> ## Documentation Index
> Fetch the complete documentation index at: https://docs.phone.wixzel.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Agents

> The prompt, the voice, and what happens when a caller asks for a person.

An agent is a prompt plus a voice engine. Everything else — the number, the
trunk, the call — is wiring around it.

```bash theme={null}
curl https://api.phone.wixzel.com/v1/agents \
  -H "Authorization: Bearer $WIXZEL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Support",
    "system_prompt": "You are a concise support agent.",
    "opening_message": "Hi, how can I help?",
    "language": "en-US",
    "voice": {
      "stt": { "model": "deepgram/nova-3", "language": "en-US" },
      "llm": { "model": "openrouter/gpt-4o-mini" },
      "tts": { "model": "elevenlabs/eleven_turbo_v2_5", "voice": "21m00Tcm4TlvDq8ikWAM" }
    }
  }'
```

`voice` is covered in [Voice engines](/voice-engines) and
[Compose a voice engine](/composing). This page is about the rest.

## Where each value comes from

Most fields are yours to write. These are the ones that reference something
else, and the endpoint that lists the valid values:

| Field                                        | Where to get it                                                           |
| -------------------------------------------- | ------------------------------------------------------------------------- |
| `voice.stt.model`, `llm.model`, `tts.model`  | [`GET /v1/engines`](/api-reference/engines) — every model, with its price |
| `voice.tts.voice`, `voice.realtime.voice`    | `GET /v1/engines/{engine}/voices`                                         |
| `language`, `voice.stt.language`             | `GET /v1/engines/{engine}/languages`                                      |
| `knowledge_base_id`                          | `GET /v1/knowledge-bases`                                                 |
| `outbound_phone_number_id`                   | `GET /v1/phone-numbers`                                                   |
| `human_transfer.destinations[].phone_number` | Your own numbers, in E.164 — not necessarily numbers on this platform     |

An id belonging to another account is reported as a `404`, not a `403`: whether
a resource exists on someone else's account is not information this API gives
out.

## Testing an agent

```bash theme={null}
curl https://api.phone.wixzel.com/v1/agents/ag_01HXYZ/test-call \
  -H "Authorization: Bearer $WIXZEL_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "to": "+14155551234" }'
```

The agent rings the number, speaks one phrase and hangs up. It answers "is my
trunk configured, and does this voice sound the way I expected" in a few seconds
without anyone having to hold a conversation with a robot. `phrase` overrides
what it says; the default is the agent's own `opening_message`, which is what a
real caller would hear first.

<Warning>
  A test call is a real call. It rings a real phone over your own carrier, and the
  meter runs exactly as it does on any other call — there is no free test mode.
  `Idempotency-Key` is required for the same reason it is on
  [`POST /v1/calls`](/quickstart).
</Warning>

For an actual conversation, place an ordinary call with `POST /v1/calls`. Human
transfer is deliberately switched off during a test call, so a test cannot tell
you whether a transfer destination answers.

## Human transfer

When a caller asks for a person, the agent can hand the live call to one.

```json theme={null}
"human_transfer": {
  "enabled": true,
  "default_destination_id": "billing",
  "ring_timeout_seconds": 20,
  "max_transfers_per_call": 3,
  "return_to_agent_on_failure": true,
  "destinations": [
    { "id": "billing", "name": "Billing team", "phone_number": "+14155550100" },
    { "id": "sales", "name": "Sales", "phone_number": "+14155550101", "ring_timeout_seconds": 45 }
  ]
}
```

<Note>
  The model is only ever told the `id` and the `name`. Numbers are resolved
  server-side at dial time, so an agent that invents a destination cannot dial an
  arbitrary number — the worst it can do is name one that does not exist. That is
  why destinations are a list you authorise rather than a number the model
  produces.
</Note>

Three rules are enforced when the agent is saved, because each of them fails on
a live call otherwise, in front of a customer who has just asked for a person:

* Destination ids must be unique.
* `default_destination_id` must name an **enabled** destination.
* `enabled: true` needs at least one enabled destination.

`max_transfers_per_call` stops a loop where the agent keeps re-dialling someone
who is not there. With `return_to_agent_on_failure`, a destination that does not
pick up hands the caller back to the agent rather than dropping the line.

<Warning>
  Transfers happen over your SIP trunk, by bridging a second leg. They are
  unavailable on a test call and in the in-browser agent test, neither of which
  has a second leg to bridge.
</Warning>

### What happened on a transfer

`GET /v1/calls/{id}` carries a `transfers` array, one entry per attempt:

```json theme={null}
"transfers": [
  {
    "destination_id": "billing",
    "destination_name": "Billing team",
    "status": "no_answer",
    "attempt": 1,
    "ring_duration_ms": 20000,
    "connected_duration_ms": 0,
    "sip_cause": 19
  }
]
```

`ring_duration_ms` is usually the fastest way to tell two very different
failures apart: a long ring ending in `no_answer` is a person who was not at
their desk, while a failure in the first second is nearly always configuration —
a number your carrier will not dial, or one it does not believe you own.
`sip_cause` is the raw Q.850 code from the carrier for the cases where that
distinction is not obvious.

## Changing an agent

`PATCH` changes only the fields you send. Two of them are replaced whole rather
than merged, because merging them would be ambiguous:

* `voice` — send the complete configuration.
* `human_transfer` — send the complete configuration, **including every
  destination you want to keep**. This is why `destinations` has no default: a
  body of `{"human_transfer": {"enabled": false}}` is rejected rather than
  quietly emptying your destination list.
