---
title: Voice
description: Agent Number voice — place and receive voice calls on the same number as SMS.
url: https://agentnumber.really.com/docs/voice
doc_version: "1.0.0"
last_updated: "2026-05-15"
---

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

# Voice

> Place and receive voice calls on the same number that handles SMS. One subscription, one phone, both channels.

## How voice works on Agent Number

Voice uses two WebSocket channels with different jobs. Both stay open during a live call.

- **Presence WS** — the signaling channel. Agent opens it once and keeps it open as long as it's accepting calls. The server pushes three event types over the call lifecycle: `{event: "ringing", call_id, caller, stream_url}` (incoming call), `{event: "answered", call_id}` (call picked up), and `{event: "ended", call_id, missed}` (call torn down).
- **Stream WS** — the audio channel. Agent opens it when it gets a `stream_url` (either from a presence `ringing` event or from a `POST /voice/dial` response). Send and receive audio as binary frames. Close it when the call ends.

The presence WS is a long-lived listener; the stream WS is per-call. They can coexist — presence keeps listening for additional incoming calls while a stream WS carries the current one.

## Inbound call flow

1. Agent opens presence WS at `wss://agentnumber.really.com/voice/presence/<phone>` with `Authorization: Bearer <token>`. Server auto-answers any incoming call.
2. External caller dials your number.
3. Server pushes `{event: "ringing", call_id, caller, stream_url}` on the presence WS.
4. Agent opens stream WS at the provided `stream_url`.
5. Agent receives caller audio as binary frames, sends agent audio back the same way.
6. On hangup (either side), stream WS closes. Presence WS stays open for the next call.

## Outbound call flow

1. Agent calls `POST` `/voice/dial` with `phone_number` (one of the subscription's owned numbers, the call's source) and `to` (destination).
2. Response: `{call_id, stream_url, direction, caller, phone_number}`. The target phone is now ringing.
3. Agent opens stream WS at `stream_url` and waits for connect.
4. On answer, audio flows both directions until hangup.

### Place a call

```bash
curl -X POST https://agentnumber.really.com/voice/dial \
  -H "authorization: Bearer <token>" \
  -H "content-type: application/json" \
  -d '{"phone_number": "+15555550123", "to": "+15551234567"}'
# → {"call_id": "...", "stream_url": "wss://...", "direction": "outbound", "caller": "+15551234567", "phone_number": "+15555550123"}
```

### Open the stream WS

Use any WS client. Audio is binary frames; the server handles cellular bridging. Auth on the stream WS uses the same bearer.

## Endpoints

| Endpoint | Auth | Purpose |
| --- | --- | --- |
| `POST` `/voice/dial` | bearer | Place outbound call. Body: `{phone_number, to}`. Returns `{call_id, stream_url, direction, caller, phone_number}`. |
| `POST` `/voice/hangup` | bearer | End an active call. Body: `{call_id}`. Returns `{call_id, device_id, status: "hangup_sent"}`. |
| `GET` `/voice/presence/<phone>` WS | bearer | Open to receive incoming-call signals. |
| `GET` `/voice/stream/<call_id>` WS | bearer | Audio stream for a specific call. |

An internal/debug-only v1 status probe exists for fleet operations, but it is not part of the public API and is intentionally not advertised pending the post-launch security review.

## Practical notes

- **Same number for SMS + voice.** No separate provisioning.
- **Real cellular, not VOIP.** Carrier-verified caller ID; downstream services treat the calls as legitimate consumer calls.
- **Per-call mic permission** is handled by the underlying hardware fleet — the agent's stream is the canonical capture path.
- **Roaming.** Numbers are US-issued and behave like normal US mobile numbers internationally; outbound dial to non-US destinations works subject to the standard carrier plan rules.

## Common errors

| Status | Endpoint | Error | Meaning |
| --- | --- | --- | --- |
| `401` | all | `missing bearer token` / `invalid bearer token` | Missing or invalid bearer — see [Auth](/docs/auth) |
| `400` | `/voice/dial`, `/voice/hangup` | `invalid_request` | Body malformed or missing required fields |
| `403` | `/voice/dial` | `not_owner` | Bearer doesn't own the specified `phone_number` |
| `404` | `/voice/dial` | `phone_number_not_found` | `phone_number` is not on an active subscription |
| `404` | `/voice/hangup`, stream WS | `call_not_found` | `call_id` doesn't exist or isn't owned by your subscription |
| `409` | `/voice/dial` | `phone_busy` | Number already on a call (single-active-call constraint per number) |
| `409` | `/voice/dial` | `subscription_not_bound` | Subscription has no `(device_id, sim_number)` slot binding |
| `502` | `/voice/dial`, `/voice/hangup` | `mac_unreachable` / `mac_dial_failed` / `phone_node_error` | Hardware adapter returned non-2xx or was unreachable |

## Sitemap

- [Overview](/docs/)
- [Quickstart](/docs/quickstart)
- [Authentication](/docs/auth)
- [SMS](/docs/sms)
- [Voice](/docs/voice)
- [MCP](/docs/mcp)
- [Payment](/docs/payment)

See the full [sitemap.md](/sitemap.md) for the complete index of docs pages.

---

← Previous: [Payment](/docs/payment) · Next: [Overview](/docs/) →
