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 presenceringingevent or from aPOST /voice/dialresponse). 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
- Agent opens presence WS at
wss://agentnumber.really.com/voice/presence/<phone>withAuthorization: Bearer <token>. Server auto-answers any incoming call. - External caller dials your number.
- Server pushes
{event: "ringing", call_id, caller, stream_url}on the presence WS. - Agent opens stream WS at the provided
stream_url. - Agent receives caller audio as binary frames, sends agent audio back the same way.
- On hangup (either side), stream WS closes. Presence WS stays open for the next call.
Outbound call flow
- Agent calls
POST/voice/dialwithphone_number(one of the subscription's owned numbers, the call's source) andto(destination). - Response:
{call_id, stream_url, direction, caller, phone_number}. The target phone is now ringing. - Agent opens stream WS at
stream_urland waits for connect. - On answer, audio flows both directions until hangup.
Place a call
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 |
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 |