# AI voice agents

Have an AI voice agent answer your number or take outbound calls, either a hosted agent over SIP or your own speech pipeline over a WebSocket.

An AI voice agent is a forward destination like any other. Point a number at it and the agent answers every inbound call. Pass it as `forwardTo` on one outbound call and the agent talks to whoever picks up. Pozy owns the phone side: the number, the carrier leg, routing, recording and the call history. The agent owns the conversation.

## Pick a shape

| | Hosted agent over SIP | Your own agent over WebSocket |
| --- | --- | --- |
| Destination | `sip:<user>@<host>` | `wss://your-server.example.com/media` |
| Where the prompt, voice and tools live | In the agent platform you use | In your own code: speech-to-text, an LLM, text-to-speech |
| What you build | Nothing on the phone side | A WebSocket server that reads and writes call audio |
| Agent can place its own calls | Use a [SIP trunk](https://pozy.ai/docs/telephony/sip-trunks) | Start calls with `POST /v1/voice/calls` |

Either way there is no second phone leg, so you pay only for the call itself.

## Point a number at the agent

```bash cURL
curl -X POST https://api.pozy.ai/v1/phone-numbers/6650d2e1b3c4d5e6f7081923/voice \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "forwardTo": "wss://agent.acme.example.com/media", "transcriptionEnabled": true }'
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/phone-numbers/6650d2e1b3c4d5e6f7081923/voice", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ forwardTo: "wss://agent.acme.example.com/media", transcriptionEnabled: true }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data.pstnForwardTo);
```
```python Python
import os, requests

resp = requests.post(
    "https://api.pozy.ai/v1/phone-numbers/6650d2e1b3c4d5e6f7081923/voice",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    json={"forwardTo": "wss://agent.acme.example.com/media", "transcriptionEnabled": True},
    timeout=30,
)
print(resp.json()["pstnForwardTo"])
```

A hosted agent works the same way with a `sip:` address in `forwardTo`. Many hosted platforms identify the number from the SIP user part: keep the number in E.164 with the leading `+`, written the same way on both sides. A `+` missing on one side is the usual reason a call connects and drops at once.

The agent sits inside the normal inbound flow, so the blocklist, business hours, an IVR option that sends one digit to the agent, and voicemail all still apply. See [Voice calls](https://pozy.ai/docs/telephony/voice-calls#route-inbound-calls).

## Hand one outbound call to an agent

Pass the agent as the per-call destination. The number's inbound setting stays as it is.

```bash
curl -X POST https://api.pozy.ai/v1/voice/calls \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Idempotency-Key: reminder-appt-7781" \
  -H "Content-Type: application/json" \
  -d '{
    "fromNumber": "+14155550142",
    "to": "+13125550188",
    "forwardTo": "wss://agent.acme.example.com/media",
    "amd": true,
    "voicemailDropMessage": "Hi, this is Acme confirming your appointment tomorrow at 10. Call us back if you need to change it."
  }'
```

With `amd: true`, the agent is only bridged in when a person answers. An answering machine gets the `voicemailDropMessage` instead.

## Build your own agent over WebSocket

When the destination is `wss://`, the call's audio streams to your server as soon as the call is answered, and audio you send back is played to the caller. Pozy does not re-encode the audio.

| Setting | Value |
| --- | --- |
| Audio you receive | The caller's side of the call |
| Codec, both directions | PCMU (G.711 u-law), 8 kHz |
| Framing | JSON text frames with an `event` field (`start`, `media`, `stop`, `error`). Audio travels base64-encoded in `media.payload`. |
| Speaking back | Write `media` frames with base64 PCMU on the same socket |
| Ending | Closing the socket ends the call |

These settings are fixed today, so build the pipeline around 8 kHz PCMU.

A typical loop:

1. Accept the WebSocket and wait for `start`.
2. Feed `media` payloads to speech-to-text.
3. Send each finished user turn to your LLM with the system prompt and tools.
4. Turn the reply into speech, encode it as PCMU at 8 kHz, and write it back as `media` frames.
5. On `stop`, or when the conversation is done, close the socket.

### Give the agent tools

Your agent's backend holds the Pozy API key, so Pozy endpoints make good agent tools. Match the call to its Pozy id with the `call.received` webhook, which fires as an inbound call starts and carries `call.id`, `from`, `to` and `conversationId`. For outbound calls, keep the `callId` from the create response.

| Tool | Endpoint |
| --- | --- |
| Hand the caller to a person | `POST /v1/voice/calls/{id}/transfer` with a `tel:` or `sip:` destination. Do this before you close the socket. |
| Hang up | `POST /v1/voice/calls/{id}/end` |
| Text a link or a summary | `POST /v1/sms/messages` from the number that was called |
| Reply in the inbox thread | The inbox send endpoints with the `conversationId` from `call.received` |
| Look up or save the caller | The [contacts](https://pozy.ai/docs/inbox/contacts) endpoints |

> **Tip:**
> 
> The same media bridge serves [WhatsApp calls](https://pozy.ai/docs/whatsapp/calling). An agent you build once can answer both channels.

## Capture the conversation

Turn on `recordingEnabled` and `transcriptionEnabled` on the number when you want Pozy to keep its own copy of the call. The recording and transcript attach to the call record after hangup. See [Call recordings](https://pozy.ai/docs/telephony/call-recordings).

## When it does not connect

| Symptom | Likely cause | What to check |
| --- | --- | --- |
| Caller hears voicemail right after answering | The WebSocket handshake failed | The `wss://` URL, TLS certificate, and that your server accepts the connection. Read `callErrors` on the call. |
| Call connects then drops at once (SIP) | The number format differs between Pozy and the agent platform | Use the same E.164 with `+` on both sides |
| `409 invalid_resource_state` on the voice settings | The number is attached to a SIP trunk | Detach it, or configure the agent on the trunk side |

If the stream cannot be set up, an already-answered caller falls to voicemail when it is on. Otherwise the call ends.

## Endpoints

| Endpoint | What it does |
| --- | --- |
| [`POST /v1/phone-numbers/{id}/voice`](https://pozy.ai/docs/api/enableVoiceOnNumber) | Enable phone calling on a number |
| [`POST /v1/voice/calls`](https://pozy.ai/docs/api/createVoiceCall) | Place an outbound phone call |
| [`POST /v1/voice/calls/{id}/transfer`](https://pozy.ai/docs/api/transferVoiceCall) | Blind-transfer a live call |
| [`POST /v1/voice/calls/{id}/end`](https://pozy.ai/docs/api/endVoiceCall) | Hang up a live call |
| [`GET /v1/calls/{id}`](https://pozy.ai/docs/api/getCall) | Get a call (any channel) |

## Webhook events

| Event | When it fires |
| --- | --- |
| [`call.received`](https://pozy.ai/docs/webhooks/events#call-received) | Call received. |
| [`call.ended`](https://pozy.ai/docs/webhooks/events#call-ended) | Call ended. |
| [`call.failed`](https://pozy.ai/docs/webhooks/events#call-failed) | Call failed. |
