# Voice sessions

## Create a realtime media session

`POST /v2/voice/sessions` requires a project key with `voice:write`:

```json
{
  "agent_id": "<project agent uuid>",
  "variables": { "account_name": "Asha" },
  "sample_rate_in": 16000,
  "sample_rate_out": 24000,
  "max_duration_seconds": 300,
  "record": false,
  "store_transcript": false,
  "metadata": { "external_id": "call_42" }
}
```

Input sample rate is 8000 or 16000 Hz; output is 8000, 16000, or 24000 Hz.
Variables are text values and are rendered into the saved agent. Capture options
inherit from the agent when omitted and can only tighten its permissions—an
explicit `true` cannot enable capture the agent disabled.

The response contains `session`, a one-use `ws_url`, and
`expires_in_seconds`. Connect the media socket before it expires. Admission
needs verified provider accounts for every stage (`503` otherwise), a free
concurrency slot, and a wallet that can cover `max_duration_seconds` (`402`
otherwise). Missing or cross-project agents are indistinguishable.

## The media socket

`wss://api.developers.dvaarik.com/v2/voice/sessions/{session_id}/stream?token=…`
is the `ws_url`. Send raw little-endian PCM16 mono frames at the input rate
as binary messages; receive PCM16 at the output rate as binary messages. Text
messages are JSON control frames:

| Direction | Frame | Meaning |
| --- | --- | --- |
| server → client | `{"type":"transcript","role":"user"\|"assistant","text":"…"}` | a finished utterance |
| server → client | `{"type":"interrupted"}` | the caller barged in; drop queued playback now |
| server → client | `{"type":"turn_complete"}` | the agent finished speaking |
| server → client | `{"type":"error","message":"…"}` | a recoverable problem |
| server → client | `{"type":"session_ended","reason":"…"}` | the server ended the call |
| client → server | `{"type":"stop"}` | hang up |

Close codes name a refusal: `4401` bad token, `4404` unknown session,
`4408` configuration expired, `4409` not connectable, `4429` concurrency
limit, `4453` provider pipeline unavailable, `1013` service restarting.

## Create an outbound BYOL session

`POST /v2/voice/sessions/outbound` requires `voice:write`. Send an
`Idempotency-Key` from your trusted server and use a number on one of your
verified carrier connections with outbound enabled:

```json
{
  "agent_id": "<project agent uuid>",
  "to": "+12025550111",
  "from": "+12025550100",
  "variables": { "account_name": "Asha" },
  "max_duration_seconds": 300,
  "metadata": { "external_id": "call_43" }
}
```

The `202` response is `{ "session": { ... }, "duplicate": false }`; a retried
key returns the first call with `duplicate: true`. Carrier account, line
ownership, routing, consent, and carrier fees remain yours.

## Read sessions

`GET /v2/voice/sessions` accepts `limit`, `offset`, `status` (`created`,
`active`, `completed`, `failed`), `agent_id`, `from`, and `to`
(ISO date-times); `GET /v2/voice/sessions/{session_id}` reads one. Both need
`voice:read`. The console equivalents back the [Calls](/console/calls) page.

A session includes status, direction, agent/version, language, a secret-free
frozen pipeline snapshot, project revision, sample rates, duration and connected
seconds, media timestamps, end reason, capture flags, metadata, and money fields.
`rate_nano_usd_per_min`, `hold_nano_usd`, and `cost_nano_usd` are decimal
**strings**. The currency is USD and `money_scale` is 1,000,000,000.

## Exact USD arithmetic

The connected-minute rate is 5,000,000 nano-USD ($0.005 — half a cent). Every
call rounds up to the next full minute — 61 connected seconds bills as 2
minutes, the same as a 120-second call. Cost is:

`ceil(connected_seconds / 60) × 5,000,000`

Use an integer or decimal library. JavaScript callers should parse money strings
with `BigInt`; do not use `Number`, `parseFloat`, or binary floating-point
division. The session freezes provider revisions, project revision, rate, and
rate version on admission; settlement is idempotent.

## Browser sessions

`POST /console/v2/projects/{project_id}/voice/sessions` is the same admission
with the account session instead of a key; it is what the
[Playground](/playground) uses. Your own product creates sessions from your
server with a project key and hands only the `ws_url` to the client — or, for
a public web page with no server in the loop, uses a publishable key and
[browser calls](/docs/browser-calls).

---

Source: https://developers.dvaarik.com/docs/voice · every page as one file: https://developers.dvaarik.com/docs.md
