# Dvaarik Developer Platform

Voice AI agents API — bring your own AI keys and your own phone line; Dvaarik
operates the realtime voice infrastructure. Every credential, machine key, limit,
agent, call, and webhook belongs to one project.

- API origin: `https://api.developers.dvaarik.com`
- Console auth: `Authorization: Bearer <account-access-token>`
- Machine auth: `X-Api-Key: <project-api-key>`
- Voice orchestration: **$0.005 per connected minute** — half a cent — rounded up to the next minute, prepaid in USD
- Pay as you go: no monthly platform fee, no minimum spend, no seat fee, no contract
- Provider and carrier usage: billed directly by the accounts you bring
- Release scope: **voice only**; this platform publishes no developer messaging-channel API

> **Release status:** projects, scoped keys, provider accounts, agents, browser
and outbound voice sessions, carrier connections and numbers, webhooks, and USD
billing are all served on two doors — a machine door that takes a project key
from your server, and a console door that takes your account session from the
browser. The console never holds a project key. The full contract is on the
[API reference](/docs/api-reference) page, derived from the backend's OpenAPI
document.

## Ownership model

Dvaarik separates three responsibilities:

1. **You own AI providers.** Connect credentials to one project. The API never
   returns the secret after save, and a call is admitted only against accounts
   the provider has confirmed.
2. **You own telephony.** Keep the carrier account, numbers, routing,
   compliance, and carrier bill. Twilio, Plivo, Exotel, FreJun/Teler, and Telnyx
   connect with your own credentials; Dvaarik hands you the URL each number must
   point at.
3. **Dvaarik owns infrastructure.** Realtime voice orchestration, interruption
   handling, project admission, exact per-minute (rounded up) metering, and
   signed, retried event delivery live here.

A missing or invalid project credential is an error. Runtime code does not
silently use a shared Dvaarik credential.

## Resource map

| Resource | Machine door (project key) | Console door (account JWT) |
| --- | --- | --- |
| Projects and keys | — | `/v2/projects`, `/v2/api-keys` |
| Provider catalogue | — | `/v2/provider-catalog` |
| Provider accounts | `/v2/provider-accounts` | `/console/v2/projects/{project_id}/provider-accounts` |
| Agents | `/v2/agents` | `/console/v2/projects/{project_id}/agents` |
| Voice sessions | `/v2/voice/sessions` (+ `/outbound`) | `/console/v2/projects/{project_id}/voice/sessions` (+ `/outbound`) |
| Carriers and numbers | `/v2/carriers` | `/console/v2/projects/{project_id}/carriers` |
| Webhooks | `/v2/projects/{project_id}/webhooks` | `/console/v2/projects/{project_id}/webhooks` |
| Billing | — | `/console/v2/billing` |

Both doors call the same service and return the same shapes, so what the
console shows is exactly what your server sees.

## Commercial and launch status

The active voice rate has no plan ladder or provider bundle. Connected seconds
are charged with fixed-point integer arithmetic against a prepaid USD wallet.
AI and carrier providers charge you separately under your own accounts.

This release is voice only: there is no developer messaging-channel API, and the
console exposes no messaging surface.

# Quickstart

## From sign-up to a signed webhook

Nine steps, in order, every one a real request. The bodies below are the shapes
the published OpenAPI document declares, and the responses are what the API
actually returns. There is no SDK yet — this is curl, with the same flow in
Node and Python at the bottom.

```bash
export DVAARIK_API="https://api.developers.dvaarik.com"
```

### 1. Create an account

```bash
curl -sS -X POST "$DVAARIK_API/console/auth/signup" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "you@example.com",
    "password": "<a long random password>",
    "name": "Your Name",
    "company": "Your Company",
    "accept_terms": true
  }'
```

**What you should see:** `202` with no body, and a code in your inbox. Confirm
it with `POST /console/auth/verify-email` and `{ "email", "code" }`. Until the
address is verified every console route answers `403`. You can also do this
step in the browser at [/signup](/signup).

### 2. Get an account session

```bash
curl -sS -X POST "$DVAARIK_API/console/auth/login" \
  -H "Content-Type: application/json" \
  -d '{ "email": "you@example.com", "password": "<password>" }'
```

```json
{
  "user": {
    "id": "3d2f…", "email": "you@example.com", "name": "Your Name",
    "company": "Your Company", "email_verified": true, "tier": "…"
  },
  "tokens": { "access_token": "eyJ…", "refresh_token": "…", "token_type": "bearer" }
}
```

**What you should see:** `200` with both tokens. Export the access token; it is
the account door and it is short-lived.

```bash
export DVAARIK_TOKEN="eyJ…"
```

Only the account session creates projects and keys. It is never sent to a
machine route, and a project key is never sent to a console route.

### 3. Create a project

```bash
curl -sS -X POST "$DVAARIK_API/v2/projects" \
  -H "Authorization: Bearer $DVAARIK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "name": "production", "external_reference": "acct_4821" }'
```

```json
{
  "id": "8b41…", "name": "production", "external_reference": "acct_4821",
  "status": "active", "revision": 1, "is_default": false,
  "voice_concurrency": 2, "api_rpm": 120, "max_call_seconds": 600,
  "social_rpm": 0, "social_daily_sends": 0,
  "created_at": "2026-09-13T09:00:00Z", "updated_at": "2026-09-13T09:00:00Z"
}
```

**What you should see:** `201` and a project id. Everything after this belongs
to it.

```bash
export PROJECT_ID="8b41…"
```

### 4. Mint a project API key

Ask for the scopes this quickstart uses and nothing else. The names are exact;
an unknown scope is refused with `400`.

```bash
curl -sS -X POST "$DVAARIK_API/v2/api-keys" \
  -H "Authorization: Bearer $DVAARIK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "backend-server",
    "project_id": "'"$PROJECT_ID"'",
    "scopes": ["provider_account:read", "provider_account:write",
               "voice:read", "voice:write",
               "webhook:read", "webhook:write"]
  }'
```

```json
{
  "key": {
    "id": "c07a…", "project_id": "8b41…", "name": "backend-server",
    "key_prefix": "dvk_1a2b", "scopes": ["provider_account:read", "…"],
    "created_at": "2026-09-13T09:01:00Z", "last_used_at": null, "revoked_at": null
  },
  "plaintext": "dvk_1a2b…"
}
```

**What you should see:** `201`, and `plaintext` exactly once. No later response
returns it. Put it in a secret manager, then:

```bash
export DVAARIK_API_KEY="dvk_1a2b…"
```

### 5. Connect one of your AI provider accounts

Read the catalogue first — it names every provider id, the credential fields to
send, and the model ids. Never hard-code a list you copied.

```bash
curl -sS "$DVAARIK_API/v2/provider-catalog" \
  -H "Authorization: Bearer $DVAARIK_TOKEN"
```

Then save a credential on the machine door and ask the provider to confirm it:

```bash
curl -sS -X POST "$DVAARIK_API/v2/provider-accounts" \
  -H "X-Api-Key: $DVAARIK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "<provider id from the catalogue>",
    "name": "Production account",
    "credentials": { "<credential field from the catalogue>": "<secret>" }
  }'

curl -sS -X POST "$DVAARIK_API/v2/provider-accounts/<account uuid>/probe" \
  -H "X-Api-Key: $DVAARIK_API_KEY"
```

```json
{
  "id": "1f9c…", "project_id": "8b41…", "provider": "…", "name": "Production account",
  "capabilities": ["stt"], "hint": { "…": "••••1a2b" },
  "status": "verified", "last_error": null,
  "key_version": 1, "revision": 2, "verified_at": "2026-09-13T09:02:10Z",
  "created_at": "2026-09-13T09:02:00Z", "updated_at": "2026-09-13T09:02:10Z"
}
```

**What you should see:** the save returns `201` with `status: "configured"`;
the probe returns `status: "verified"` with a `verified_at`. A rejected
credential comes back `invalid` with the provider's own message in
`last_error`, and a transient provider failure leaves it `configured` rather
than guessing. **Only a verified account is admitted into an agent or a call.**

A cascade agent needs three roles — speech-to-text, language model,
text-to-speech — from one account or three. A realtime agent needs one
realtime-capable account.

### 6. Create an agent

Model ids come from the catalogue; `account_id` values are the provider
accounts you verified in step 5.

```bash
curl -sS -X POST "$DVAARIK_API/v2/agents" \
  -H "X-Api-Key: $DVAARIK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "support",
    "prompt": "You are the receptionist for {{company}}. Answer questions and book visits.",
    "pipeline_mode": "cascade",
    "pipeline_config": {
      "stt": { "account_id": "<uuid>", "model": "<catalogue model id>" },
      "llm": { "account_id": "<uuid>", "model": "<catalogue model id>" },
      "tts": { "account_id": "<uuid>", "model": "<catalogue model id>", "voice": "<provider voice id>" }
    },
    "language": "en-IN",
    "greeting": "Hello, this is support.",
    "variables": [{ "name": "company", "required": true }]
  }'
```

```json
{
  "agent": {
    "id": "a44e…", "project_id": "8b41…", "name": "support", "version": 1,
    "is_active": true, "pipeline_mode": "cascade", "language": "en-IN",
    "has_webhook_secret": true, "…": "…"
  },
  "webhook_secret": "whsec_…"
}
```

**What you should see:** `201`, and `webhook_secret` once. Store it if you set
the agent's own `webhook_url`; `POST /v2/agents/{agent_id}/rotate-secret`
issues a new one.

```bash
export AGENT_ID="a44e…"
```

### 7. Start a call

**A browser session.** Your server asks for the session and hands the client
only the returned URL — never the key.

```bash
curl -sS -X POST "$DVAARIK_API/v2/voice/sessions" \
  -H "X-Api-Key: $DVAARIK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "'"$AGENT_ID"'",
    "variables": { "company": "Northline Dental" },
    "sample_rate_in": 16000,
    "sample_rate_out": 24000,
    "max_duration_seconds": 300
  }'
```

```json
{
  "session": { "id": "9c1b…", "status": "created", "direction": "inbound",
               "cost_nano_usd": "0", "rate_nano_usd_per_min": "5000000",
               "money_scale": 1000000000, "currency": "USD", "…": "…" },
  "ws_url": "wss://api.developers.dvaarik.com/v2/voice/sessions/9c1b…/stream?token=…",
  "expires_in_seconds": 60
}
```

**What you should see:** `201` with a single-use `ws_url`. Connect before
`expires_in_seconds` elapses, send little-endian PCM16 mono at
`sample_rate_in`, and read PCM16 at `sample_rate_out`; JSON text frames carry
transcripts and turn control. The frame and close-code tables are on
[Voice sessions](/docs/voice).

**Or an outbound call** on a number you already own, once a carrier is
connected under [Telephony](/docs/telephony):

```bash
curl -sS -X POST "$DVAARIK_API/v2/voice/sessions/outbound" \
  -H "X-Api-Key: $DVAARIK_API_KEY" \
  -H "Idempotency-Key: <unique per call attempt>" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "'"$AGENT_ID"'",
    "to": "+15550000002",
    "from": "+15550000001",
    "max_duration_seconds": 300
  }'
```

**What you should see:** `202` with `{ "session": { … }, "duplicate": false }`.
Replaying the same `Idempotency-Key` returns the first call with
`duplicate: true` instead of dialling twice.

To hear an agent without writing any client code, use the
[playground](/playground) — it opens a browser session with your account
session, so no key ever reaches a browser.

### 8. Receive a webhook

```bash
curl -sS -X POST "$DVAARIK_API/v2/projects/$PROJECT_ID/webhooks" \
  -H "X-Api-Key: $DVAARIK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "primary",
    "url": "https://example.com/hooks/dvaarik",
    "event_types": ["call.completed", "call.failed"]
  }'
```

```json
{
  "id": "6ee0…", "project_id": "8b41…", "name": "primary",
  "url": "https://example.com/hooks/dvaarik",
  "event_types": ["call.completed", "call.failed"],
  "status": "active", "revision": 1, "secret_hint": "••••9f2c",
  "signing_secret": "whsec_…",
  "created_at": "2026-09-13T09:05:00Z", "updated_at": "2026-09-13T09:05:00Z"
}
```

Then send yourself a ping:

```bash
curl -sS -X POST "$DVAARIK_API/v2/projects/$PROJECT_ID/webhooks/<endpoint uuid>/test" \
  -H "X-Api-Key: $DVAARIK_API_KEY"
```

**What you should see:** the create returns `201` with `signing_secret` once —
store it now. The test returns `202` with a `delivery_id`, meaning the delivery
was **recorded**, not that it arrived. Your endpoint then receives a
`webhook.ping` carrying `X-Webhook-Signature: t=<unix>,v1=<hex>`. Verify it
before parsing: the signed bytes are `"<timestamp>.<raw body>"`. Copy-paste
verifiers for Node and Python are on [Webhooks](/docs/webhooks). Deliveries
that exhaust their retries appear under `GET .../webhooks/dead-letters`.

### 9. Fund the wallet

A session is refused with `402` when the spendable balance cannot cover its
maximum duration, so top up before the first real call. Voice orchestration is
**$0.005 per connected minute, rounded up to the next minute**; your AI
providers and your carrier bill you directly for the same call. There is no
monthly fee and no minimum spend — you top up what you want, spend what you
use, and the rest stays on the wallet. Top up under [Account](/console) and see
[Billing](/docs/billing).

## The same flow in Node

No SDK exists yet. Plain `fetch` on Node 18 or newer is the whole client.

```js
const API = "https://api.developers.dvaarik.com";

async function call(path, { token, key, method = "GET", body } = {}) {
  const res = await fetch(API + path, {
    method,
    headers: {
      ...(token ? { Authorization: "Bearer " + token } : {}),
      ...(key ? { "X-Api-Key": key } : {}),
      ...(body ? { "Content-Type": "application/json" } : {}),
    },
    body: body ? JSON.stringify(body) : undefined,
  });
  const text = await res.text();
  const data = text ? JSON.parse(text) : null;
  if (!res.ok) throw new Error(res.status + " " + JSON.stringify(data?.detail ?? data));
  return data;
}

const { tokens } = await call("/console/auth/login", {
  method: "POST",
  body: { email: process.env.EMAIL, password: process.env.PASSWORD },
});
const token = tokens.access_token;

const project = await call("/v2/projects", { token, method: "POST", body: { name: "production" } });
const created = await call("/v2/api-keys", {
  token,
  method: "POST",
  body: { name: "backend-server", project_id: project.id, scopes: ["voice:read", "voice:write"] },
});
const key = created.plaintext;          // the only time you will see it

const session = await call("/v2/voice/sessions", {
  key,
  method: "POST",
  body: { agent_id: process.env.AGENT_ID, sample_rate_in: 16000, sample_rate_out: 24000 },
});
console.log(session.ws_url, "expires in", session.expires_in_seconds, "s");
```

Money fields are integer nano-USD **strings**. Parse them with `BigInt`, never
`Number`: `BigInt(session.session.cost_nano_usd)`.

## The same flow in Python

```python
import os, requests

API = "https://api.developers.dvaarik.com"

def call(path, token=None, key=None, method="GET", body=None):
    headers = {}
    if token: headers["Authorization"] = f"Bearer {token}"
    if key:   headers["X-Api-Key"] = key
    res = requests.request(method, API + path, headers=headers, json=body, timeout=30)
    if res.status_code >= 400:
        raise RuntimeError(f"{res.status_code} {res.text}")
    return res.json() if res.content else None

token = call("/console/auth/login", method="POST",
             body={"email": os.environ["EMAIL"], "password": os.environ["PASSWORD"]})["tokens"]["access_token"]

project = call("/v2/projects", token=token, method="POST", body={"name": "production"})
created = call("/v2/api-keys", token=token, method="POST", body={
    "name": "backend-server", "project_id": project["id"],
    "scopes": ["voice:read", "voice:write"],
})
key = created["plaintext"]              # the only time you will see it

session = call("/v2/voice/sessions", key=key, method="POST", body={
    "agent_id": os.environ["AGENT_ID"], "sample_rate_in": 16000, "sample_rate_out": 24000,
})
print(session["ws_url"], "expires in", session["expires_in_seconds"], "s")
```

Use `int()` or `decimal.Decimal` on the nano-USD strings; never `float`.

## Where to go next

- [Authentication](/docs/authentication) — the two doors, and what a `401` means.
- [API reference](/docs/api-reference) — one page per resource, every operation with a curl example.
- [Errors](/docs/errors) — every status this API returns and what to do about it.

# Authentication

## Two credentials, two jobs

### Account session

The web console uses a short-lived access JWT and rotating refresh token. Send
the access token as a bearer token only to console endpoints:
`/v2/projects`, `/v2/api-keys`, `/v2/provider-catalog`, and everything under
`/console/v2/`.

`401` may mean the access token expired. The first refresh rotates both tokens;
concurrent console requests share that refresh and retry once. A `403` is an
authorization decision and must not trigger token refresh.

### Project API key

Machine endpoints use:

`X-Api-Key: dvk_...`

The key identifies its project on the server. Do not send a different project ID
in a body and expect it to widen access. Keep the plaintext in a server-side
secret manager. Never embed it in JavaScript shipped to a browser, a mobile
bundle, a public repository, logs, or analytics.

Keys are stored as hashes. Creation is the only response that contains the
plaintext; revoke and replace a lost key. A project key presented to a
`/console/v2/` route authenticates nothing.

# Projects and keys

## Projects

List projects with `GET /v2/projects?limit=25&offset=0` and read one with
`GET /v2/projects/{project_id}`, using an account JWT.

A project response contains `id`, `name`, optional `external_reference`,
`status`, `revision`, `is_default`, the voice concurrency, API rate and
maximum call-second limits, and timestamps.

Create with `POST /v2/projects`:

```json
{
  "name": "Production",
  "external_reference": "customer_42"
}
```

The name is required and limited to 120 characters. The reference is optional.

## Scoped keys

Create with `POST /v2/api-keys` and an account JWT:

```json
{
  "name": "Production server",
  "project_id": "00000000-0000-0000-0000-000000000000",
  "scopes": ["project:read", "voice:read", "voice:write"]
}
```

These nine scopes exist, spelled exactly this way. An unknown scope is refused
with `400`, so copy them rather than guessing a plural:

| Scope | What it opens |
| --- | --- |
| `project:read` | reserved; no `/v2` route enforces it today |
| `voice:read` | list and read agents and voice sessions |
| `voice:write` | create, update and delete agents; start sessions |
| `provider_account:read` | list and read provider accounts |
| `provider_account:write` | save, probe, rotate and disconnect them |
| `carrier:read` | carrier catalogue, connections and numbers |
| `carrier:write` | connect, verify, disconnect; import, attach, update and delete numbers |
| `webhook:read` | list endpoints and dead letters |
| `webhook:write` | create, test and disable endpoints (it mints a signing secret) |

Request only the scopes a server actually needs — a key handed to a partner
app to start sessions should not also be able to disconnect the carrier
account those sessions run on. The [API reference](/docs/api-reference) names
the scope on every machine operation.

The creation response is `{ "key": { ...safe metadata }, "plaintext": "..." }`.
Plaintext appears once. List with `GET /v2/api-keys?project_id=...`; revoke with
`DELETE /v2/api-keys/{key_id}`.

# Provider accounts

## Render from the server catalogue

`GET /v2/provider-catalog` with an account JWT returns provider IDs, labels,
capabilities (`stt`, `llm`, `tts`, `realtime`), credential fields and
alternatives, model IDs, and provider pricing links. Treat it as the source of
truth. Do not copy model or secret-field lists into an application.

The catalogue describes shape only. It never reads configured credentials and
never calls a provider.

## Save, verify, use

Console: `POST /console/v2/projects/{project_id}/provider-accounts`

```json
{
  "provider": "<catalogue id>",
  "name": "Production account",
  "credentials": { "<catalogue field>": "<secret>" }
}
```

A save validates the exact shape and encrypts the secret; the account is
**configured**. `POST .../provider-accounts/{account_id}/probe` asks the
provider: a confirmed credential becomes **verified** (with `verified_at`), a
rejected one **invalid** (with `last_error`), and a transient provider failure
stays **configured** so an uncertain probe never admits a call. The console runs
the probe right after saving. Only verified accounts are accepted in an agent's
pipeline and at session admission.

Safe responses contain `id`, `project_id`, provider, name, capabilities, a
masked `hint`, status, safe error, revision, key version, verification timestamp,
and ordinary timestamps. They never contain credential material.
`POST .../rotate` re-encrypts the stored secret under the current keyring key
without changing the credential.

Server applications may use `/v2/provider-accounts` with the corresponding
project-key scopes; browsers must not.

# Agents

## Two doors, one agent

- Machine: `POST/GET /v2/agents`, `GET/PATCH/DELETE /v2/agents/{agent_id}`,
  `POST /v2/agents/{agent_id}/rotate-secret` with `voice:read`/`voice:write`.
- Console: the same operations under
  `/console/v2/projects/{project_id}/agents` with the account session — what
  the [Agents](/console/agents) page uses.

## Create a cascade agent

Model IDs should come from `/v2/provider-catalog`; account IDs must belong to
the project and be verified. Voice IDs are the provider's own voice names.

```json
{
  "name": "Support",
  "prompt": "Help the caller with {{account_name}}.",
  "pipeline_mode": "cascade",
  "pipeline_config": {
    "stt": { "account_id": "<uuid>", "model": "<catalogue model>" },
    "llm": { "account_id": "<uuid>", "model": "<catalogue model>" },
    "tts": { "account_id": "<uuid>", "model": "<catalogue model>", "voice": "<provider voice>" }
  },
  "language": "en-IN",
  "greeting": "Hello, this is Support.",
  "variables": [{ "name": "account_name", "required": true }]
}
```

A realtime agent uses `pipeline_mode: "realtime"` and exactly one
`realtime` stage with `account_id`, `model`, and `voice`. Arbitrary
pipeline keys are rejected so plaintext credentials cannot be hidden in an
agent configuration.

The contract also carries an optional display name, tool declarations and a
`tool_webhook_url`, DTMF, recording and transcript flags, duration and idle
limits, retention, backchannel and ambience settings, and a call `webhook_url`
with transcript/recording inclusion flags. Responses add project ID, version,
active state, timestamps, and `has_webhook_secret`.

`record_calls` asks **your carrier** to record; the audio lands in your own
carrier account and Dvaarik keeps only the carrier's reference to it. A
browser call has no carrier, so it has no recording. `store_transcript`
writes the transcript to the Dvaarik database, where it is kept for 90 days
and then deleted; the short `insight_config` summary survives that deletion.
See [Billing → What we store](/docs/billing).

## The webhook secret

Creation and `rotate-secret` return `webhook_secret` once. It signs the
completed-call delivery to the agent's own `webhook_url` using the same
timestamped scheme as project webhooks — see [Webhooks](/docs/webhooks).

## Delete

`DELETE` deactivates the agent and returns `{ id, name, deleted, calls }`;
historical sessions keep their agent id and version. Numbers bound to the agent
stop answering.

# 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).

# Browser calls

## A call button on your own website

A visitor clicks, allows the microphone, and talks to your agent. No server in
the loop, no secret in the page.

That works because a **publishable key** is a different kind of credential
from a project key. It is bound to one agent, refused on any origin you have
not listed, rate-limited per key and per visitor, and it can do exactly one
thing: start a browser voice call. It is meant to be read by anyone who views
your page source.

| | Project key `dvk_live_…` | Publishable key `pk_live_…` |
| --- | --- | --- |
| Lives on | your server | your public web page |
| Header | `X-Api-Key` | `X-Publishable-Key` |
| Can reach | every `/v2/*` route its scopes allow | `POST /v2/browser/sessions`, nothing else |
| Bound to | a project | one agent in one project |
| Origin-checked | no | yes, against the key's own list |
| If it leaks | rotate immediately, assume compromise | it works only on origins you allowed |

```bash
npm install @dvaarik/web
```

## 1. Mint a key

Open [Publishable keys](/console/publishable-keys) in the console, pick an
active agent whose [provider accounts](/docs/providers) are all verified, and
list the origins the button will run on. The console shows the full key value —
it is not a secret, and hiding it would only push you to keep a copy somewhere
less safe.

## 2. Paste two lines

```html
<script src="https://cdn.jsdelivr.net/npm/@dvaarik/web/dist/dvaarik-web.global.js"></script>
<dvaarik-call
  publishable-key="pk_live_YOUR_KEY"
  agent-id="YOUR_AGENT_ID"
  label="Talk to us"
></dvaarik-call>
```

That is a real `<button>` in a shadow root with a live status line —
"Allow the microphone…", "Connecting…", "Live · 00:14", and any refusal in
plain words. It is keyboard reachable, announced to screen readers, and it
releases the microphone when the visitor navigates away. Style it from the
host page:

```css
dvaarik-call { --dvaarik-accent: #0f766e; --dvaarik-radius: 8px; }
dvaarik-call::part(button) { letter-spacing: .02em; }
```

| Attribute | Required | Meaning |
| --- | --- | --- |
| `publishable-key` | yes | `pk_live_…` from the console |
| `agent-id` | yes | the agent the key was minted for |
| `label` | no | button text when idle |
| `end-label` | no | button text during a call |
| `base-url` | no | API origin, for a staging deployment |

`agent_id` is in the request body because the API requires it, not because
the page gets to choose: the key is bound to one agent and any other id is the
same `404` an id that never existed would get. The console prints the right
value beside the key.

## 3. Or drive it yourself

```tsx
import { useEffect, useRef, useState } from "react";
import { DvaarikCall, type DvaarikCallStatus, type DvaarikTranscript } from "@dvaarik/web";

export function CallButton() {
  const call = useRef<DvaarikCall | null>(null);
  const [status, setStatus] = useState<DvaarikCallStatus>("idle");
  const [lines, setLines] = useState<DvaarikTranscript[]>([]);
  const [problem, setProblem] = useState<string | null>(null);
  const [minutes, setMinutes] = useState(0);

  useEffect(() => {
    const instance = new DvaarikCall({
      publishableKey: process.env.NEXT_PUBLIC_DVAARIK_PUBLISHABLE_KEY!,
      agentId: process.env.NEXT_PUBLIC_DVAARIK_AGENT_ID!,
    });
    call.current = instance;
    const off = [
      instance.on("status", setStatus),
      instance.on("transcript", (line) => setLines((all) => [...all, line])),
      instance.on("error", (error) => setProblem(error.message)),
    ];
    // Unmounting must release the microphone.
    return () => {
      off.forEach((unsubscribe) => unsubscribe());
      void instance.stop();
    };
  }, []);

  useEffect(() => {
    if (status !== "live") return;
    const tick = setInterval(() => setMinutes(call.current?.minutesBilled ?? 0), 1000);
    return () => clearInterval(tick);
  }, [status]);

  const live = status === "live";
  return (
    <>
      <button
        type="button"
        disabled={status === "requesting-mic" || status === "connecting"}
        // Straight from the click: the audio context is resumed inside the
        // gesture, or autoplay policy means the agent is never heard.
        onClick={() => void (live ? call.current?.stop() : call.current?.start())}
      >
        {live ? "End call" : "Talk to us"}
      </button>
      {live && <span>{minutes} min so far</span>}
      {problem && <p role="alert">{problem}</p>}
      <ol>{lines.map((line, i) => <li key={i}>{line.role}: {line.text}</li>)}</ol>
    </>
  );
}
```

`status` moves `idle → requesting-mic → connecting → live → ended`, or to
`error`. `mute(true)` silences the microphone at the source.
`elapsedSeconds` and `minutesBilled` let a page show the running cost.

## What the key cannot do

There is no list, no read, no configuration, and no outbound dial behind a
publishable key. `POST /v2/browser/sessions` returns four fields —
`session_id`, `ws_url`, `expires_in_seconds`, `max_duration_seconds` — and
nothing about your project, your rate, your wallet, or your pipeline.

Two refusals are deliberately vague, and the SDK stays vague with them. An
unfunded wallet and a dead speech provider are both `503`, separated only by
a machine code, because a visitor who could tell those apart would be reading
your business over your shoulder. A wrong agent, a foreign agent, and an agent
that never existed are one `404`, so a scraped key cannot be walked across
your project.

| Code | Cause |
| --- | --- |
| `MicPermissionDenied` | the visitor refused the prompt, or policy blocked it |
| `MicUnavailable` | no device, or the page is not on HTTPS |
| `InvalidKey` | `401` — unknown or revoked key |
| `OriginNotAllowed` | `403` — this origin is not on the key's list |
| `AgentNotFound` | `404` — wrong or foreign agent |
| `InvalidRequest` | `422` — usually a missing prompt variable |
| `RateLimited` | `429`, or close `4429` |
| `TemporarilyUnavailable` | `503`, or close `1013` |
| `ProviderUnavailable` | `503`, or close `4453` |
| `SessionEnded` | close `4401`, `4404`, `4408`, `4409` |
| `NetworkError` | nothing came back, or the socket dropped |

Every one carries a sentence you may show a visitor as-is, plus `status`,
`closeCode`, `serverCode` and `retryAfterSeconds` for your logs.

## Origins

An origin is `scheme://host[:port]` — no path, no query, no credentials.
`https://acme.com` and `https://acme.com:443` are the same origin and both
work. Up to 20 per key. A key with **no** origins refuses every request, which
is the safe state for a key you have not finished setting up; the console says
so on the row.

The one wildcard is a loopback port — `http://localhost:*` and
`http://127.0.0.1:*` — because you cannot know which port your dev server
will pick, and the alternative is that everybody develops with the allowlist
switched off.

**Origins cannot be edited.** There is no route that changes them, on purpose:
where a key may run is part of what the key is. To change the list, create a
key with the origins you want, redeploy the page with the new value, then
revoke the old key. Rotating a key keeps its origins and limits and replaces
only the value — that is the tool for "this key leaked", not for "this key
should run somewhere else".

## Limits

Every limit has a conservative default, so a key created without touching any
of them is already bounded. There is no unlimited value; the smallest is 1,
which makes "no traffic at all" something you express by revoking the key.

| Limit | Default | Range |
| --- | --- | --- |
| Concurrent sessions | 3 | 1–50 |
| Sessions per minute (whole key) | 10 | 1–120 |
| Sessions per minute (one visitor) | 5 | 1–60 |
| Longest call, seconds | 300 | 1–3600 |

The effective ceiling on one call is the **minimum** of the key's, the agent's
and the project's. A key cannot raise a limit set anywhere else. A project may
hold up to 10 live browser keys, a separate budget from your project keys, so
minting call buttons cannot exhaust your server-key allowance.

## Billing

A browser call is not a cheaper call. It runs through the same admission, the
same verified [provider pipeline](/docs/providers), the same project
concurrency lease and the same wallet hold as a call from your own server, and
it settles the same way: whole connected minutes, **rounded up**, at
$0.005 per minute. One second is one minute; sixty-one seconds is two. See
[Billing](/docs/billing).

A browser call has no carrier in the path, so there is nothing to record and
no recording reference on the call. `store_transcript` still applies, on the
same 90-day window as any other call.

`minutesBilled` on the call object is `ceil(elapsedSeconds / 60)` — the
honest number to show a visitor while they talk. The settled figure on the
call record, visible under [Calls](/console/calls), is the one you are
charged.

## The socket, if you are not using the SDK

`ws_url` already carries the session's single-use token; do not append one.
Send raw PCM16 mono at 16 kHz as binary frames and play the raw PCM16 mono at
24 kHz that comes back. JSON control frames are `ready`, `transcript`
(`{role, text}`), `interrupted` (drop everything you have queued — this is
barge-in), `turn_complete`, `error`, and `session_ended`. Send
`{"type":"stop"}` to hang up. The close codes are the ones in the table above.

# Telephony (BYO carrier)

## Connect a carrier you already pay for

`GET /console/v2/projects/{project_id}/carriers/catalog` (or
`GET /v2/carriers/catalog`) lists the connectable carriers with the exact
credential fields to render and, per carrier, whether it **fetches an answer
URL** or **takes a stream URL**. The console form is generated from it.

`POST .../carriers` with `{ "provider", "name", "credentials" }` verifies the
credentials against the carrier straight away. A `201` is returned either way:
`status` is `verified` or `invalid` with `last_error`, and a rejected
credential is never stored. `POST .../carriers/{connection_id}/verify`
re-checks stored credentials, or replaces them when given a body — only if the
carrier accepts the replacement. `DELETE` forgets the credentials and disables
every number on the connection; your carrier account is never touched.

## Numbers

`POST .../carriers/{connection_id}/numbers/import` pulls the numbers your
account holds (Twilio, Plivo, Exotel, Telnyx). `POST .../carriers/{connection_id}/numbers`
attaches one E.164 number by hand for a carrier that cannot list them
(FreJun/Teler). Each number row carries `answer_url` **or** `stream_url` —
the exact string to paste into your carrier — plus `inbound_agent_id`,
`inbound_enabled`, and `outbound_enabled`.

`PATCH .../carriers/numbers/{number_id}` binds an active agent
(`inbound_agent_id`), switches answering on or off (`inbound_enabled`), and
allows or forbids dialling from the number (`outbound_enabled`). Inbound
requires a verified connection and a bound agent. `DELETE` forgets the number
locally.

## Carrier setup guides

The URLs below are the pattern; the console shows the concrete per-number value
with its guard segment. `{number_id}` and `{guard}` are minted by Dvaarik.

### Twilio

Credentials: Account SID and Auth token (Twilio Console home). Style: answer URL.

Paste `https://api.developers.dvaarik.com/inbound/twilio/{number_id}/{guard}`
into the number's **Voice → A call comes in → Webhook** (GET or POST both work).
Twilio fetches TwiML from it and opens a bidirectional media stream to Dvaarik.

### Plivo

Credentials: Auth ID and Auth token (Plivo console overview). Style: answer URL.

Create a Plivo application whose **Answer URL** is
`https://api.developers.dvaarik.com/inbound/plivo/{number_id}/{guard}` and
assign the number to it. Plivo fetches XML from it and opens the stream.

### Exotel

Credentials: Account SID, API key, API token, and the API host
(`api.in.exotel.com` for accounts in India). Style: stream URL.

In the number's app flow, point the **Voicebot** applet at
`wss://api.developers.dvaarik.com/inbound/exotel/stream/{number_id}/{guard}`.
Exotel connects the bidirectional stream directly.

### FreJun / Teler

Credentials: API token and, for Teler-branded accounts, the API base URL.
Style: stream URL. Numbers are attached by hand.

Set the application's stream URL to
`wss://api.developers.dvaarik.com/inbound/frejun/stream/{number_id}/{guard}`.

### Telnyx

Credentials: API key (Telnyx portal → API Keys) and webhook public key
(Account Settings → Keys & Credentials → Public Key — Telnyx signs every
webhook with it and no API returns it, so it must be pasted). Add the Call
Control Application ID only if you also want to dial out; inbound works
without it. Style: answer URL.

Telnyx binds the webhook to the Call Control **Application**, not to the
phone number, so several numbers answered by different agents need one
application each. On first connect: create the application in your Telnyx
portal, connect the account here, import your numbers, then paste each
number's `https://api.developers.dvaarik.com/inbound/telnyx/{number_id}/{guard}`
into that number's own application. Telnyx signs the webhook body itself
(Ed25519 over `timestamp|body`, not the URL) and an outbound call dials
bare — Dvaarik starts the audio stream only once Telnyx reports the call
answered, so nothing is charged while the phone is still ringing.

## Outbound

`POST /v2/voice/sessions/outbound` dials `to` from `from`, which must be a
number on a verified connection with `outbound_enabled`. The carrier bills the
leg; Dvaarik bills connected minutes, rounded up. Verify TRAI/DND and 140-series
obligations with your carrier before dialling Indian numbers.

# Webhooks

## Endpoints

Console: `/console/v2/projects/{project_id}/webhooks`. Machine:
`/v2/projects/{project_id}/webhooks` with `webhook:read` to list and
`webhook:write` to create, test or disable (the path project
must match the key's project).

`POST` with `{ "name", "url", "event_types" }` creates an endpoint. The URL
must be public HTTPS on port 443 with no credentials or fragment. Event types are
a closed set for this voice release: `call.completed`, `call.failed`,
`webhook.ping`, and the wildcards `call.*`, `webhook.*`, `*`. The `201`
contains `signing_secret` **once**; no route returns it again. Up to 10
endpoints per project.

`GET` lists endpoints (`include_disabled=true` to see disabled ones, which
keep `secret_hint` and `revision`). `DELETE .../{endpoint_id}` disables the
endpoint, bumps its revision, and cancels queued deliveries. `POST
.../{endpoint_id}/test` queues a signed `webhook.ping` and returns `202`
with `delivery_id` and `queued` — recorded, not yet sent. `GET
.../dead-letters` lists deliveries that exhausted their retries with
`event_type`, `endpoint_id`, `attempts`, `max_attempts`, and `last_error`;
payloads are not retained on terminal rows.

## Delivery and signature

Every delivery is an HTTP `POST` with a JSON body and these headers:

| Header | Value |
| --- | --- |
| `X-Webhook-Event` | the event name, e.g. `call.completed` |
| `X-Webhook-Timestamp` | unix seconds when the delivery was signed |
| `X-Webhook-Signature` | `t=<timestamp>,v1=<hex HMAC-SHA256>` |
| `X-Webhook-Delivery` | the delivery id; deduplicate retries on it |
| `X-Dvaarik-Event`, `X-Dvaarik-Signature` | legacy aliases; `sha256=<hex>` over the body alone — do not build new verifiers on them |

The signed bytes are `"<timestamp>.<raw body>"` and the key is the endpoint's
signing secret. Verify before parsing, compare in constant time, and reject a
timestamp outside your tolerance window (five minutes is usual) to stop replays.
Respond `2xx` promptly; anything else is retried with backoff and then
dead-lettered.

```js
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyDvaarikWebhook(req, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(String(req.get("X-Webhook-Signature") ?? "").split(",").map((kv) => kv.split("=")));
  const timestamp = req.get("X-Webhook-Timestamp");
  if (!parts.t || !parts.v1 || parts.t !== timestamp) return false;
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceSeconds) return false;
  const expected = createHmac("sha256", secret).update(`${timestamp}.`).update(req.body).digest("hex");
  const a = Buffer.from(expected, "hex"), b = Buffer.from(parts.v1, "hex");
  return a.length === b.length && timingSafeEqual(a, b);
}
```

```python
import hmac, hashlib, time

def verify_dvaarik_webhook(headers, raw_body: bytes, secret: str, tolerance_s=300) -> bool:
    parts = dict(kv.split("=", 1) for kv in headers.get("X-Webhook-Signature", "").split(",") if "=" in kv)
    timestamp = headers.get("X-Webhook-Timestamp", "")
    if not parts.get("t") or not parts.get("v1") or parts["t"] != timestamp:
        return False
    if abs(time.time() - int(timestamp)) > tolerance_s:
        return False
    expected = hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])
```

The agent-level `webhook_url` (set on the agent) is signed the same way with
the agent's own `webhook_secret`.

# Billing

## One rate, prepaid in USD

`GET /console/v2/billing/rate` returns the rate on its own:
`rate_nano_usd_per_minute: "5000000"` (**$0.005 per connected minute** — half
a cent), `metering_basis: "connected_minute_rounded_up"`, and a
`rate_version`. Every call rounds up to the next full minute. Cost is
`ceil(connected_seconds / 60) × rate` in integer nano-USD; a 59-second call
costs $0.005 (1 minute), a 61-second call costs $0.01 (2 minutes), and a
10-minute call costs $0.05.

Read the rate from this route rather than hard-coding it. A published rate
change is a new `rate_version`; sessions already admitted keep the version
they froze.

## Pure pay as you go

There is no monthly platform fee, no minimum spend, no seat fee, no
commitment, and no contract. You top up a prepaid wallet with whatever amount
you want, you spend what you use, and whatever is left stays yours.

Connected voice minutes are the only metered thing on this platform. Dvaarik
charges nothing for:

- creating projects, agents, or API keys;
- the console;
- the SDKs (`@dvaarik/node`, `dvaarik`, `@dvaarik/web`);
- webhook delivery, retries, and dead letters;
- the [playground](/playground), beyond its own connected minutes — a
  playground call is a real call, billed at the same rate as any other;
- post-call analysis. Insights run on **your** model under **your** provider
  account, so that provider bills you for those tokens and Dvaarik adds
  nothing on top;
- **text and chat.** We sell voice minutes only. A website chat bot built on
  your own model costs you nothing from us.

`GET /console/v2/billing/summary?project_id=…` returns the account wallet —
`balance_nano_usd`, `held_nano_usd`, `spendable_nano_usd`,
`lifetime_topup_nano_usd`, `low_balance_threshold_nano_usd`, `low_balance`
— together with the rate and the project's concurrency, request-rate, and
maximum-session limits. The wallet belongs to the account; it is reported
against one project.

## Holds and settlement

A session places a hold for its maximum duration when admitted. It is refused
with `402` when the spendable balance cannot cover that hold. When the session
ends, the hold is released and the connected seconds are settled exactly once.
`GET /console/v2/billing/transactions?limit=50&offset=0` lists the ledger
newest first; `amount_nano_usd` is null on archival rows from before USD.

## Top up

1. `GET /console/v2/billing/quote?amount_usd_cents=2500` — no fee, no markup:
   one cent buys exactly 10,000,000 nano-USD. Bounds are $10 to $2,000.
2. `POST /console/v2/billing/topup` with `{ "amount_usd_cents", "project_id" }`
   creates a Razorpay USD order and records it first; the response carries
   `order_id` and `razorpay_key_id`, which the console hands to Razorpay
   Checkout. `502` means the gateway did not create an order; `503` means the
   order could not be recorded and no checkout was returned.
3. The payment webhook credits the wallet. The console polls the summary until
   `lifetime_topup_nano_usd` grows; a successful Checkout alone is never
   treated as credit.

Whether international cards are accepted depends on Razorpay international-payment
enablement for the Dvaarik merchant account; the console reports the gateway's
answer rather than assuming it.

## Receipts

`GET /console/v2/billing/invoices` lists receipts for paid top-ups;
`GET /console/v2/billing/invoices/{invoice_id}.pdf` returns the PDF. A receipt
exists only once a payment has landed.

## What we store

Storage is not a line on your bill here, because there is almost nothing of
yours on our disks.

**Call recordings live in your own phone company's account.** When an agent
has `record_calls` enabled, Dvaarik asks your carrier to record the call.
The audio is written into your own carrier account under your own carrier
plan. Dvaarik stores only the carrier's reference to that recording, so you
fetch the audio from your carrier with your own credentials. We pay nothing to
store it, and you are not charged twice for storing it.

**Browser calls have no recording.** A browser call has no carrier in the
path, so there is no carrier to record it and no recording reference on the
call.

**Transcripts live in the Dvaarik database for 90 days and are then
deleted.** `store_transcript` controls whether one is written at all;
`retention_days` cannot extend a transcript past that 90-day window. Pull
anything you need to keep — through the API or your call webhook — inside it.

**The insight block survives the transcript.** The short post-call summary
produced by `insight_config` stays on the call record after the transcript
is deleted: it is small, and it is your business record of what the call was
about. It is generated on your own model, on your own provider account.

## Estimate the per-minute cost before you build

The platform fee is exact and published — $0.005 per connected minute,
rounded up to the next minute. What you cannot know from this API alone is
what your *chosen* speech, language-model, voice, or realtime provider will
charge for that same minute, because that is billed by them, on your account,
at whatever rate you negotiated.

The console's cost-estimate card (agent create/edit, the agent list, and the
[pricing](/pricing) calculator) renders a breakdown — Listen / Think / Speak
or Realtime, your carrier's rate typed in by hand, and the platform fee — from
a small table of public list prices in `src/lib/providerListPrices.ts`,
dated and sourced per provider. It is always an estimate: only the platform
fee row is exact, computed from the same nano-USD constants as the rest of
this page; every provider row can drift the moment that provider changes its
own price list.

# SDKs

## Three official SDKs

| Package | Language | Install | Door |
| --- | --- | --- | --- |
| `@dvaarik/node` | TypeScript / JavaScript, Node 20+ | `npm install @dvaarik/node` | machine |
| `dvaarik` | Python 3.9+ | `pip install dvaarik` | machine |
| `@dvaarik/web` | TypeScript / JavaScript, browser | `npm install @dvaarik/web` | browser |

The two **server** SDKs wrap the machine door: the `/v2/*` routes that
authenticate with a project key (`X-Api-Key: dvk_live_…`). Neither wraps the
`/console/*` routes, which take an account browser session and do not belong
on a server. `@dvaarik/web` is the odd one out and is described
[below](#dvaarikweb-browser-calls).

They share one shape on purpose — the same namespaces, the same error
hierarchy, the same retry and idempotency rules, the same pagination helper,
and the same webhook verifier — so a team running one service in Node and
another in Python reads one mental model. Neither SDK is required: every
operation is an ordinary HTTPS request, documented on the
[API reference](/docs/api-reference).

The Node package runs on any fetch-capable runtime (Bun, Deno, Cloudflare
Workers, Vercel Edge) and ships ESM, CommonJS, and type declarations. The
Python package ships a sync `Dvaarik` and an async `AsyncDvaarik` over one
transport, with a `py.typed` marker and `httpx` as its only dependency.

> **Release status.** The source lives in the platform repository under
`sdk/typescript`, `sdk/python` and `sdk/web`. Publishing to npm and PyPI is a
manual release step; until the first publish lands, install from those
directories.

## 60-second quickstart

Create an agent, ring a number, verify the delivery that reports the result.
You need a project API key from [Projects and keys](/docs/projects-and-keys),
a `verified` [provider account](/docs/providers), and a
[carrier number](/docs/telephony) with outbound enabled.

```ts
import { Dvaarik } from "@dvaarik/node";

const client = new Dvaarik({
  apiKey: process.env.DVAARIK_API_KEY!,        // never hard-code this
  projectId: process.env.DVAARIK_PROJECT_ID!,  // fills the webhook routes' path
});

// 1 — Create an agent. `webhook_secret` comes back exactly once: store it.
const { agent, webhook_secret } = await client.agents.create({
  name: "Reception",
  prompt: "You are the receptionist for Blue Orchid Salon. Book appointments.",
  greeting: "Blue Orchid, how can I help?",
  pipeline_mode: "cascade",
  pipeline_config: { stt: "sarvam", llm: "gemini", tts: "elevenlabs" },
  language: "en-IN",
});

// 2 — Ring a customer. The idempotency key is what makes a timed-out request
//     safe to send again: the repeat returns the first call with
//     `duplicate: true` instead of dialling twice.
const { session, duplicate } = await client.voiceSessions.createOutbound(
  { agent_id: agent.id, to: "+919876543210", from: "+911140000000" },
  { idempotencyKey: orderId },
);

// 3 — Verify the delivery. RAW BYTES, before any JSON parsing.
app.post("/webhooks/dvaarik", express.raw({ type: "application/json" }), async (req, res) => {
  try {
    const delivery = await client.webhooks.verify(req.body, req.headers, webhook_secret);
    if (delivery.event === "call.completed") handle(delivery.payload);
    res.sendStatus(200);
  } catch {
    res.sendStatus(400); // bad signature, or outside the five-minute window
  }
});
```

```python
import os
from dvaarik import Dvaarik, DvaarikWebhookSignatureError

client = Dvaarik(
    api_key=os.environ["DVAARIK_API_KEY"],        # never hard-code this
    project_id=os.environ["DVAARIK_PROJECT_ID"],  # fills the webhook routes' path
)

# 1 — Create an agent. webhook_secret comes back exactly once: store it.
created = client.agents.create({
    "name": "Reception",
    "prompt": "You are the receptionist for Blue Orchid Salon. Book appointments.",
    "greeting": "Blue Orchid, how can I help?",
    "pipeline_mode": "cascade",
    "pipeline_config": {"stt": "sarvam", "llm": "gemini", "tts": "elevenlabs"},
    "language": "en-IN",
})
agent, webhook_secret = created.agent, created.webhook_secret

# 2 — Ring a customer. The idempotency key makes a timed-out request safe to
#     send again: the repeat comes back duplicate=True instead of dialling twice.
result = client.voice_sessions.create_outbound(
    {"agent_id": agent.id, "to": "+919876543210", "from": "+911140000000"},
    idempotency_key=order_id,
)

# 3 — Verify the delivery. RAW BYTES, before any JSON parsing.
@app.post("/webhooks/dvaarik")
async def dvaarik_webhook(request: Request) -> Response:
    try:
        delivery = client.webhooks.verify(await request.body(), request.headers, webhook_secret)
    except DvaarikWebhookSignatureError:
        return Response(status_code=400)
    if delivery.event == "call.completed":
        handle(delivery.payload)
    return Response(status_code=200)
```

Deduplicate on `delivery.deliveryId` / `delivery.delivery_id`: retries of the
same delivery reuse it.

## Namespaces

| Node | Python | Routes |
| --- | --- | --- |
| `client.agents` | `client.agents` | `/v2/agents` — list, create, get, update, delete, rotate secret |
| `client.voiceSessions` | `client.voice_sessions` | `/v2/voice/sessions` and `/outbound` — list, create, get |
| `client.providerAccounts` | `client.provider_accounts` | `/v2/provider-accounts` — list, save, get, probe, rotate, disconnect |
| `client.carriers` | `client.carriers` | `/v2/carriers` — list, catalog, create, get, verify, disconnect |
| `client.carrierNumbers` | `client.carrier_numbers` | `/v2/carriers/numbers` — list, attach, import, update, delete |
| `client.webhooks` | `client.webhooks` | `/v2/projects/{project_id}/webhooks` — list, create, disable, test, dead letters, **verify** |
| `client.projects` | `client.projects` | `/v2/projects` — account token, not a project key |
| `client.apiKeys` | `client.api_keys` | `/v2/api-keys` — account token, not a project key |

`projects`, `apiKeys` and the provider catalogue are the account door: minting
a project or a key is account administration that a project-scoped key
deliberately cannot do. Construct a second client with an account access token
for those, and calling one without it fails locally rather than sending a
request that would come back `401`.

## What both SDKs do for you

- **Typed errors.** One base carrying `status` and the request id, with a class
  per status: auth (401/403), payment required (402), not found (404),
  conflict (409), validation (422, exposing the field errors), rate limit (429,
  exposing `Retry-After`), and server (5xx). See [Errors](/docs/errors).
- **Retries.** Exponential backoff with jitter on `429`, `502` and `503` only,
  honouring `Retry-After`. A `POST` is never retried unless you supplied an
  idempotency key — an outbound call that timed out may already be ringing.
- **Pagination.** `iterate()` walks a limit/offset list route as an async
  iterator, with `pages()`, `all()` and a `maxItems` bound.
- **Webhook verification.** The timestamped HMAC described in
  [Webhooks](/docs/webhooks), including the replay-window check, in one call.
- **Rate-limit visibility.** `client.rateLimit` / `client.rate_limit` reports
  the `X-RateLimit-*` headers from the most recent response, so you can pace
  against `remaining` instead of waiting to be refused.

Money stays an integer nano-USD **string** in both SDKs. Parse it with
`BigInt` in JavaScript and `int` in Python; never with `Number` or `float`.
The platform fee is $0.005 per connected minute, rounded up to the next
minute — see [Billing](/docs/billing).

## `@dvaarik/web` — browser calls

The browser SDK is not a fourth client with the same shape. It runs in a
public web page with a **publishable** key (`pk_live_…`), and the only route
that key can reach is `POST /v2/browser/sessions` — one voice call, on one
agent, from an origin on the key's list. So it has no namespaces, no
pagination and no webhook verifier: none of those belong in a page a stranger
can view the source of. What it has instead is a microphone, a socket, and a
call button.

```html
<script src="https://cdn.jsdelivr.net/npm/@dvaarik/web/dist/dvaarik-web.global.js"></script>
<dvaarik-call publishable-key="pk_live_…" agent-id="…" label="Talk to us"></dvaarik-call>
```

```ts
import { DvaarikCall } from "@dvaarik/web";

const call = new DvaarikCall({ publishableKey: "pk_live_…", agentId: "…" });
call.on("status", (status) => render(status));      // idle → live → ended
call.on("transcript", (line) => append(line));       // { role, text, final }
call.on("error", (error) => show(error.message));    // a sentence, already visitor-safe
button.addEventListener("click", () => call.start());
```

Zero runtime dependencies, ESM + CommonJS + a single-file `<script>` build,
and `elapsedSeconds` / `minutesBilled` so a page can show the running cost
honestly. Mint keys under
[Publishable keys](/console/publishable-keys); the full guide, including
origins, limits and every error code, is on
[Browser calls](/docs/browser-calls).

## Not in the SDKs

This release is **voice only**, and no SDK exposes anything else.
`@dvaarik/web` deliberately stops at starting a call: minting, rotating and
revoking publishable keys is console work behind an account session, not
something a public page may do.

# API reference

## One page per resource

Generated from the backend's published OpenAPI document — regenerate with `npm run api:reference`, and `npm run test:contract` fails when it drifts. Each page lists every operation with its door, parameters, request and response fields, declared errors, and a runnable curl example.

| Resource | Operations | What it is for |
| --- | --- | --- |
| [Projects API](/docs/api/projects) | 3 | A project is the isolation boundary: credentials, keys, agents, numbers, sessions and webhooks all belong to exactly one. |
| [API keys](/docs/api/api-keys) | 3 | Project-scoped machine keys. |
| [Provider accounts](/docs/api/provider-accounts) | 13 | Your own AI credentials, encrypted per project. |
| [Agents API](/docs/api/agents) | 12 | An agent is the frozen configuration a call runs against: pipeline, prompt, language, limits and webhook. |
| [Voice sessions](/docs/api/voice-sessions) | 8 | One session is one call. |
| [Carriers and numbers](/docs/api/carriers) | 22 | Bring your own telephony account. |
| [Webhooks API](/docs/api/webhooks) | 10 | Signed, retried event delivery per project. |
| [Billing API](/docs/api/billing) | 7 | A prepaid USD wallet per account. |

## Every operation

### [Projects API](/docs/api/projects)

| Method | Path | Door | Scope |
| --- | --- | --- | --- |
| `GET` | [`/v2/projects`](/docs/api/projects#get-v2-projects) | account session | — |
| `POST` | [`/v2/projects`](/docs/api/projects#post-v2-projects) | account session | — |
| `GET` | [`/v2/projects/{project_id}`](/docs/api/projects#get-v2-projects-project-id) | account session | — |

### [API keys](/docs/api/api-keys)

| Method | Path | Door | Scope |
| --- | --- | --- | --- |
| `GET` | [`/v2/api-keys`](/docs/api/api-keys#get-v2-api-keys) | account session | — |
| `POST` | [`/v2/api-keys`](/docs/api/api-keys#post-v2-api-keys) | account session | — |
| `DELETE` | [`/v2/api-keys/{key_id}`](/docs/api/api-keys#delete-v2-api-keys-key-id) | account session | — |

### [Provider accounts](/docs/api/provider-accounts)

| Method | Path | Door | Scope |
| --- | --- | --- | --- |
| `GET` | [`/v2/provider-accounts`](/docs/api/provider-accounts#get-v2-provider-accounts) | project key | `provider_account:read` |
| `POST` | [`/v2/provider-accounts`](/docs/api/provider-accounts#post-v2-provider-accounts) | project key | `provider_account:write` |
| `DELETE` | [`/v2/provider-accounts/{account_id}`](/docs/api/provider-accounts#delete-v2-provider-accounts-account-id) | project key | `provider_account:write` |
| `GET` | [`/v2/provider-accounts/{account_id}`](/docs/api/provider-accounts#get-v2-provider-accounts-account-id) | project key | `provider_account:read` |
| `POST` | [`/v2/provider-accounts/{account_id}/probe`](/docs/api/provider-accounts#post-v2-provider-accounts-account-id-probe) | project key | `provider_account:write` |
| `POST` | [`/v2/provider-accounts/{account_id}/rotate`](/docs/api/provider-accounts#post-v2-provider-accounts-account-id-rotate) | project key | `provider_account:write` |
| `GET` | [`/console/v2/projects/{project_id}/provider-accounts`](/docs/api/provider-accounts#get-console-v2-projects-project-id-provider-accounts) | account session | — |
| `POST` | [`/console/v2/projects/{project_id}/provider-accounts`](/docs/api/provider-accounts#post-console-v2-projects-project-id-provider-accounts) | account session | — |
| `DELETE` | [`/console/v2/projects/{project_id}/provider-accounts/{account_id}`](/docs/api/provider-accounts#delete-console-v2-projects-project-id-provider-accounts-account-id) | account session | — |
| `GET` | [`/console/v2/projects/{project_id}/provider-accounts/{account_id}`](/docs/api/provider-accounts#get-console-v2-projects-project-id-provider-accounts-account-id) | account session | — |
| `POST` | [`/console/v2/projects/{project_id}/provider-accounts/{account_id}/probe`](/docs/api/provider-accounts#post-console-v2-projects-project-id-provider-accounts-account-id-probe) | account session | — |
| `POST` | [`/console/v2/projects/{project_id}/provider-accounts/{account_id}/rotate`](/docs/api/provider-accounts#post-console-v2-projects-project-id-provider-accounts-account-id-rotate) | account session | — |
| `GET` | [`/v2/provider-catalog`](/docs/api/provider-accounts#get-v2-provider-catalog) | account session | — |

### [Agents API](/docs/api/agents)

| Method | Path | Door | Scope |
| --- | --- | --- | --- |
| `GET` | [`/v2/agents`](/docs/api/agents#get-v2-agents) | project key | `voice:read` |
| `POST` | [`/v2/agents`](/docs/api/agents#post-v2-agents) | project key | `voice:write` |
| `DELETE` | [`/v2/agents/{agent_id}`](/docs/api/agents#delete-v2-agents-agent-id) | project key | `voice:write` |
| `GET` | [`/v2/agents/{agent_id}`](/docs/api/agents#get-v2-agents-agent-id) | project key | `voice:read` |
| `PATCH` | [`/v2/agents/{agent_id}`](/docs/api/agents#patch-v2-agents-agent-id) | project key | `voice:write` |
| `POST` | [`/v2/agents/{agent_id}/rotate-secret`](/docs/api/agents#post-v2-agents-agent-id-rotate-secret) | project key | `voice:write` |
| `GET` | [`/console/v2/projects/{project_id}/agents`](/docs/api/agents#get-console-v2-projects-project-id-agents) | account session | — |
| `POST` | [`/console/v2/projects/{project_id}/agents`](/docs/api/agents#post-console-v2-projects-project-id-agents) | account session | — |
| `DELETE` | [`/console/v2/projects/{project_id}/agents/{agent_id}`](/docs/api/agents#delete-console-v2-projects-project-id-agents-agent-id) | account session | — |
| `GET` | [`/console/v2/projects/{project_id}/agents/{agent_id}`](/docs/api/agents#get-console-v2-projects-project-id-agents-agent-id) | account session | — |
| `PATCH` | [`/console/v2/projects/{project_id}/agents/{agent_id}`](/docs/api/agents#patch-console-v2-projects-project-id-agents-agent-id) | account session | — |
| `POST` | [`/console/v2/projects/{project_id}/agents/{agent_id}/rotate-secret`](/docs/api/agents#post-console-v2-projects-project-id-agents-agent-id-rotate-secret) | account session | — |

### [Voice sessions](/docs/api/voice-sessions)

| Method | Path | Door | Scope |
| --- | --- | --- | --- |
| `GET` | [`/v2/voice/sessions`](/docs/api/voice-sessions#get-v2-voice-sessions) | project key | `voice:read` |
| `POST` | [`/v2/voice/sessions`](/docs/api/voice-sessions#post-v2-voice-sessions) | project key | `voice:write` |
| `GET` | [`/v2/voice/sessions/{session_id}`](/docs/api/voice-sessions#get-v2-voice-sessions-session-id) | project key | `voice:read` |
| `POST` | [`/v2/voice/sessions/outbound`](/docs/api/voice-sessions#post-v2-voice-sessions-outbound) | project key | `voice:write` |
| `GET` | [`/console/v2/projects/{project_id}/voice/sessions`](/docs/api/voice-sessions#get-console-v2-projects-project-id-voice-sessions) | account session | — |
| `POST` | [`/console/v2/projects/{project_id}/voice/sessions`](/docs/api/voice-sessions#post-console-v2-projects-project-id-voice-sessions) | account session | — |
| `GET` | [`/console/v2/projects/{project_id}/voice/sessions/{session_id}`](/docs/api/voice-sessions#get-console-v2-projects-project-id-voice-sessions-session-id) | account session | — |
| `POST` | [`/console/v2/projects/{project_id}/voice/sessions/outbound`](/docs/api/voice-sessions#post-console-v2-projects-project-id-voice-sessions-outbound) | account session | — |

### [Carriers and numbers](/docs/api/carriers)

| Method | Path | Door | Scope |
| --- | --- | --- | --- |
| `GET` | [`/v2/carriers`](/docs/api/carriers#get-v2-carriers) | project key | `carrier:read` |
| `POST` | [`/v2/carriers`](/docs/api/carriers#post-v2-carriers) | project key | `carrier:write` |
| `DELETE` | [`/v2/carriers/{connection_id}`](/docs/api/carriers#delete-v2-carriers-connection-id) | project key | `carrier:write` |
| `GET` | [`/v2/carriers/{connection_id}`](/docs/api/carriers#get-v2-carriers-connection-id) | project key | `carrier:read` |
| `POST` | [`/v2/carriers/{connection_id}/numbers`](/docs/api/carriers#post-v2-carriers-connection-id-numbers) | project key | `carrier:write` |
| `POST` | [`/v2/carriers/{connection_id}/numbers/import`](/docs/api/carriers#post-v2-carriers-connection-id-numbers-import) | project key | `carrier:write` |
| `POST` | [`/v2/carriers/{connection_id}/verify`](/docs/api/carriers#post-v2-carriers-connection-id-verify) | project key | `carrier:write` |
| `GET` | [`/v2/carriers/catalog`](/docs/api/carriers#get-v2-carriers-catalog) | project key | `carrier:read` |
| `GET` | [`/v2/carriers/numbers`](/docs/api/carriers#get-v2-carriers-numbers) | project key | `carrier:read` |
| `DELETE` | [`/v2/carriers/numbers/{number_id}`](/docs/api/carriers#delete-v2-carriers-numbers-number-id) | project key | `carrier:write` |
| `PATCH` | [`/v2/carriers/numbers/{number_id}`](/docs/api/carriers#patch-v2-carriers-numbers-number-id) | project key | `carrier:write` |
| `GET` | [`/console/v2/projects/{project_id}/carriers`](/docs/api/carriers#get-console-v2-projects-project-id-carriers) | account session | — |
| `POST` | [`/console/v2/projects/{project_id}/carriers`](/docs/api/carriers#post-console-v2-projects-project-id-carriers) | account session | — |
| `DELETE` | [`/console/v2/projects/{project_id}/carriers/{connection_id}`](/docs/api/carriers#delete-console-v2-projects-project-id-carriers-connection-id) | account session | — |
| `GET` | [`/console/v2/projects/{project_id}/carriers/{connection_id}`](/docs/api/carriers#get-console-v2-projects-project-id-carriers-connection-id) | account session | — |
| `POST` | [`/console/v2/projects/{project_id}/carriers/{connection_id}/numbers`](/docs/api/carriers#post-console-v2-projects-project-id-carriers-connection-id-numbers) | account session | — |
| `POST` | [`/console/v2/projects/{project_id}/carriers/{connection_id}/numbers/import`](/docs/api/carriers#post-console-v2-projects-project-id-carriers-connection-id-numbers-import) | account session | — |
| `POST` | [`/console/v2/projects/{project_id}/carriers/{connection_id}/verify`](/docs/api/carriers#post-console-v2-projects-project-id-carriers-connection-id-verify) | account session | — |
| `GET` | [`/console/v2/projects/{project_id}/carriers/catalog`](/docs/api/carriers#get-console-v2-projects-project-id-carriers-catalog) | account session | — |
| `GET` | [`/console/v2/projects/{project_id}/carriers/numbers`](/docs/api/carriers#get-console-v2-projects-project-id-carriers-numbers) | account session | — |
| `DELETE` | [`/console/v2/projects/{project_id}/carriers/numbers/{number_id}`](/docs/api/carriers#delete-console-v2-projects-project-id-carriers-numbers-number-id) | account session | — |
| `PATCH` | [`/console/v2/projects/{project_id}/carriers/numbers/{number_id}`](/docs/api/carriers#patch-console-v2-projects-project-id-carriers-numbers-number-id) | account session | — |

### [Webhooks API](/docs/api/webhooks)

| Method | Path | Door | Scope |
| --- | --- | --- | --- |
| `GET` | [`/v2/projects/{project_id}/webhooks`](/docs/api/webhooks#get-v2-projects-project-id-webhooks) | project key | `webhook:read` |
| `POST` | [`/v2/projects/{project_id}/webhooks`](/docs/api/webhooks#post-v2-projects-project-id-webhooks) | project key | `webhook:write` |
| `DELETE` | [`/v2/projects/{project_id}/webhooks/{endpoint_id}`](/docs/api/webhooks#delete-v2-projects-project-id-webhooks-endpoint-id) | project key | `webhook:write` |
| `POST` | [`/v2/projects/{project_id}/webhooks/{endpoint_id}/test`](/docs/api/webhooks#post-v2-projects-project-id-webhooks-endpoint-id-test) | project key | `webhook:write` |
| `GET` | [`/v2/projects/{project_id}/webhooks/dead-letters`](/docs/api/webhooks#get-v2-projects-project-id-webhooks-dead-letters) | project key | `webhook:read` |
| `GET` | [`/console/v2/projects/{project_id}/webhooks`](/docs/api/webhooks#get-console-v2-projects-project-id-webhooks) | account session | — |
| `POST` | [`/console/v2/projects/{project_id}/webhooks`](/docs/api/webhooks#post-console-v2-projects-project-id-webhooks) | account session | — |
| `DELETE` | [`/console/v2/projects/{project_id}/webhooks/{endpoint_id}`](/docs/api/webhooks#delete-console-v2-projects-project-id-webhooks-endpoint-id) | account session | — |
| `POST` | [`/console/v2/projects/{project_id}/webhooks/{endpoint_id}/test`](/docs/api/webhooks#post-console-v2-projects-project-id-webhooks-endpoint-id-test) | account session | — |
| `GET` | [`/console/v2/projects/{project_id}/webhooks/dead-letters`](/docs/api/webhooks#get-console-v2-projects-project-id-webhooks-dead-letters) | account session | — |

### [Billing API](/docs/api/billing)

| Method | Path | Door | Scope |
| --- | --- | --- | --- |
| `GET` | [`/console/v2/billing/invoices`](/docs/api/billing#get-console-v2-billing-invoices) | account session | — |
| `GET` | [`/console/v2/billing/invoices/{invoice_id}.pdf`](/docs/api/billing#get-console-v2-billing-invoices-invoice-id-pdf) | account session | — |
| `GET` | [`/console/v2/billing/quote`](/docs/api/billing#get-console-v2-billing-quote) | account session | — |
| `GET` | [`/console/v2/billing/rate`](/docs/api/billing#get-console-v2-billing-rate) | account session | — |
| `GET` | [`/console/v2/billing/summary`](/docs/api/billing#get-console-v2-billing-summary) | account session | — |
| `POST` | [`/console/v2/billing/topup`](/docs/api/billing#post-console-v2-billing-topup) | account session | — |
| `GET` | [`/console/v2/billing/transactions`](/docs/api/billing#get-console-v2-billing-transactions) | account session | — |

## Conventions

- Origin: `https://api.developers.dvaarik.com`.
- Machine door: `X-Api-Key: $DVAARIK_API_KEY`, from a trusted server only.
- Account door: `Authorization: Bearer $DVAARIK_TOKEN`, the console session.
- Money is an integer nano-USD decimal string; parse it with `BigInt`, never `Number`.
- Every error body is `{ "detail": … }`; see the [errors page](/docs/errors).
- There is no SDK yet. The examples are curl, `fetch`, and `requests`.
- To try a call without writing code, use the [playground](/playground) rather than pasting a key into a browser.

# Errors

## The error body

Every failure is FastAPI's own envelope — no second wrapper, no vendor code:

```json
{ "detail": "API key is missing the required scope 'voice:write'" }
```

`detail` is a sentence for the developer. On a `422` it is instead an array of
`{ loc, msg, type }` validation items, where `loc` names the offending field.
One retired route returns a structured `{ code, message, migrate_to }` object,
which is why the declared type is `string | object` rather than `string`.

## Every status this API returns

The cause column is the backend's own declaration — from the OpenAPI responses and `devplatform/schemas/api_errors.py` — not a generic HTTP table. A status that is not listed here is not one this API raises deliberately.

| Status | What causes it | What to do |
| --- | --- | --- |
| `400` | A hard limit was reached: too many projects, or too many active keys on this project. A requested scope is not a scope this platform defines. | Fix the request or free a slot — the message names the limit that was hit. |
| `401` | No account session was presented, or it is invalid or expired. `Missing API key` or `Invalid API key` — the `X-Api-Key` header is absent, unknown, or revoked. | Re-authenticate. For a console session refresh once, then send the user to sign in; for a project key, check the header and that the key is not revoked. |
| `402` | Wallet balance cannot cover this session's maximum duration. Wallet balance cannot cover this call's maximum duration. | Top up the wallet, or lower `max_duration_seconds` so the hold fits the spendable balance. |
| `403` | The account is suspended or its email is not verified. `API key is not scoped to a project`, `Project unavailable`, or `API key is missing the required scope '<scope>'`. | Verify the account's email, or mint a key with the scope the message names. Never retry unchanged. |
| `404` | The project or the addressed resource does not exist for this owner. A resource owned by another developer is deliberately indistinguishable from one that never existed. | Treat as absent, never as a permissions hint — another owner's resource and one that never existed are deliberately the same answer. |
| `409` | The project or resource is in a state that refuses this change. | Read the state and resolve it — verify the carrier, re-enable the endpoint, rename the duplicate — then retry. |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. | The body failed schema validation. `detail` is an array of `{loc, msg, type}`; the `loc` path names the offending field. |
| `429` | The project's `api_rpm` ceiling was exceeded. Honour `Retry-After`; `X-RateLimit-*` headers are on every response. | Back off for `Retry-After` seconds. `X-RateLimit-Limit`, `-Remaining` and `-Reset` are on every response, so pace before you are refused. |
| `502` | The payment gateway did not create an order. | An upstream (payment gateway, carrier) did not answer usefully. Safe to retry with backoff. |
| `503` | The order could not be recorded locally, so no checkout was returned. BYOK provider accounts are unverified/unavailable, or the service is draining. The carrier connection or BYOK pipeline is unavailable. Webhook secret storage (the credential keyring) is unavailable. | A dependency is unavailable or the service is draining. Retry with backoff; do not treat it as a permanent failure. |


## Rate limits

Machine routes are counted per project against that project's `api_rpm`, in a
fixed one-minute window. Every response — not only a refusal — carries:

| Header | Meaning |
| --- | --- |
| `X-RateLimit-Limit` | the project's requests per minute |
| `X-RateLimit-Remaining` | what is left in this window |
| `X-RateLimit-Reset` | seconds until the window rolls |
| `Retry-After` | on a `429` only; wait this long |

Pace against `X-RateLimit-Remaining` rather than waiting to be refused. The
limiter fails **open**: if its store is unreachable your requests are allowed
through, because a limiter protects other tenants and is not a security
control.

## WebSocket close codes

The media socket refuses with an application close code rather than a status:

| Code | Meaning |
| --- | --- |
| `4401` | bad or expired stream token, or a token/session mismatch |
| `4404` | unknown voice session |
| `4408` | the session configuration expired before you connected |
| `4409` | the session is not connectable, or the project changed after admission |
| `4429` | the project's concurrency limit is full |
| `4453` | the provider pipeline is unavailable |
| `1013` | the service is restarting; reconnect with a new session |

## Retrying safely

- `429`, `502`, `503` — retry with exponential backoff and jitter. Honour
  `Retry-After` when it is present.
- `402` — do not retry until the wallet is funded; the answer will not change.
- `400`, `403`, `404`, `409`, `422` — never retry unchanged. Each one names
  something you must alter in the request or in the resource's state.
- Outbound calls take an `Idempotency-Key`. Reuse the same key when you retry
  so a network timeout cannot dial twice; the repeat returns the first call
  with `duplicate: true`.

## Truthful states

The API reports what it actually knows, and the console shows those words
verbatim rather than smoothing them over:

- A saved credential is **configured** until the provider confirms it; only a
  **verified** account takes calls. A provider outage during a probe leaves it
  **configured**, never optimistically verified.
- A carrier connection the carrier rejected is **invalid**, carrying the
  carrier's own message.
- A voice session is billed for connected seconds only, rounded up to whole
  minutes, and settled exactly once.
- A webhook test returns `202` when the delivery is **recorded**, not when it
  is delivered. Read `dead-letters` for deliveries that never landed.

# Projects API

## What this covers

A project is the isolation boundary: credentials, keys, agents, numbers, sessions and webhooks all belong to exactly one. Create one per environment or per customer.

Every example below runs as written once these are exported:

```bash
export DVAARIK_TOKEN="<account access token>"   # console session, from POST /console/auth/login
export PROJECT_ID="<uuid>"
```

A `<value>` inside a JSON body is a value you supply. Responses are JSON; every error body is `{ "detail": … }`. Each schema is expanded once per page, where it first appears.

### Account door errors

| Status | Cause |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. Refresh once, then re-authenticate. |
| `403` | The account is suspended or its email is not verified. |

## Operations — account session

The same resource through the console door. These are what the web console calls; both doors return the same shapes.

### GET `/v2/projects`

List the projects on this account.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `list_projects_v2_projects_get` |
| Success | `200` `ProjectInfo[]` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `limit` | query | integer | no |  |
| `offset` | query | integer | no |  |

#### `ProjectInfo`

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `api_rpm` | integer | yes |  |
| `created_at` | date-time | yes |  |
| `external_reference` | string or null | yes |  |
| `id` | uuid | yes |  |
| `is_default` | boolean | yes |  |
| `max_call_seconds` | integer | yes |  |
| `name` | string | yes |  |
| `revision` | integer | yes |  |
| `social_daily_sends` | integer | yes |  |
| `social_rpm` | integer | yes |  |
| `status` | string | yes |  |
| `updated_at` | date-time | yes |  |
| `voice_concurrency` | integer | yes |  |

| Error | When |
| --- | --- |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X GET "https://api.developers.dvaarik.com/v2/projects?limit=20" \
  -H "Authorization: Bearer $DVAARIK_TOKEN"
```

### POST `/v2/projects`

Create a project — the isolation boundary every other resource hangs off.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `create_project_v2_projects_post` |
| Success | `201` `ProjectInfo` |

Request body: `CreateProjectRequest`.

#### `CreateProjectRequest`

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `external_reference` | string or null | no |  |
| `name` | string | yes |  |

| Error | When |
| --- | --- |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X POST "https://api.developers.dvaarik.com/v2/projects" \
  -H "Authorization: Bearer $DVAARIK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "production",
    "external_reference": "acct_4821"
  }'
```

### GET `/v2/projects/{project_id}`

Read one project with its concurrency, request-rate and maximum-session limits.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `get_project_v2_projects__project_id__get` |
| Success | `200` `ProjectInfo` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `project_id` | path | uuid | yes |  |

| Error | When |
| --- | --- |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X GET "https://api.developers.dvaarik.com/v2/projects/$PROJECT_ID" \
  -H "Authorization: Bearer $DVAARIK_TOKEN"
```

# API keys

## What this covers

Project-scoped machine keys. The plaintext is returned once, at creation, and never again; a key carries an explicit scope set and cannot be widened after the fact.

Every example below runs as written once these are exported:

```bash
export DVAARIK_TOKEN="<account access token>"   # console session, from POST /console/auth/login
export KEY_ID="<uuid>"
```

A `<value>` inside a JSON body is a value you supply. Responses are JSON; every error body is `{ "detail": … }`. Each schema is expanded once per page, where it first appears.

### Account door errors

| Status | Cause |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. Refresh once, then re-authenticate. |
| `403` | The account is suspended or its email is not verified. |

## Operations — account session

The same resource through the console door. These are what the web console calls; both doors return the same shapes.

### GET `/v2/api-keys`

List the project's keys: prefix, scopes and usage timestamps, never the plaintext.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `list_v2_keys_v2_api_keys_get` |
| Success | `200` `ApiKeyInfo[]` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `project_id` | query | uuid or null | no |  |
| `limit` | query | integer | no |  |
| `offset` | query | integer | no |  |
| `include_revoked` | query | boolean | no |  |

#### `ApiKeyInfo`

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `created_at` | date-time | yes |  |
| `id` | uuid | yes |  |
| `key_prefix` | string | yes |  |
| `last_used_at` | date-time or null | yes |  |
| `name` | string | yes |  |
| `project_id` | uuid or null | yes |  |
| `revoked_at` | date-time or null | yes |  |
| `scopes` | string[] | yes |  |

| Error | When |
| --- | --- |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X GET "https://api.developers.dvaarik.com/v2/api-keys?limit=20" \
  -H "Authorization: Bearer $DVAARIK_TOKEN"
```

### POST `/v2/api-keys`

Mint a project key with an explicit scope set. This is the only response that ever carries the plaintext.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `create_v2_key_v2_api_keys_post` |
| Success | `201` `CreatedKeyResponse` |

Request body: `CreateV2KeyRequest`.

#### `CreateV2KeyRequest`

Canonical v2 create with a required, explicit permission set.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `name` | string | yes |  |
| `project_id` | uuid | yes |  |
| `scopes` | string[] | yes | min 1 item(s); max 32 item(s) |

#### `CreatedKeyResponse`

The ONLY response that ever carries the plaintext.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `key` | ApiKeyInfo | yes |  |
| `plaintext` | string | yes |  |

| Error | When |
| --- | --- |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X POST "https://api.developers.dvaarik.com/v2/api-keys" \
  -H "Authorization: Bearer $DVAARIK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "backend-server",
    "project_id": "<project uuid>",
    "scopes": [
      "voice:read",
      "voice:write"
    ]
  }'
```

### DELETE `/v2/api-keys/{key_id}`

Revoke a key immediately. Revocation cannot be undone; mint a replacement first if the key is live.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `revoke_v2_key_v2_api_keys__key_id__delete` |
| Success | `200` `ApiKeyInfo` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `key_id` | path | uuid | yes |  |

| Error | When |
| --- | --- |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X DELETE "https://api.developers.dvaarik.com/v2/api-keys/$KEY_ID" \
  -H "Authorization: Bearer $DVAARIK_TOKEN"
```

# Provider accounts

## What this covers

Your own AI credentials, encrypted per project. A saved account is `configured` until the provider confirms it; only a `verified` account is admitted into an agent pipeline or a call.

Every example below runs as written once these are exported:

```bash
export DVAARIK_API_KEY="dvk_..."        # project API key, server side only
export DVAARIK_TOKEN="<account access token>"   # console session, from POST /console/auth/login
export ACCOUNT_ID="<uuid>"
export PROJECT_ID="<uuid>"
```

A `<value>` inside a JSON body is a value you supply. Responses are JSON; every error body is `{ "detail": … }`. Each schema is expanded once per page, where it first appears.

### Machine door errors

Raised by the project-key dependency before any handler on this page runs.

| Status | Cause |
| --- | --- |
| `401` | `Missing API key` or `Invalid API key` — the `X-Api-Key` header is absent, unknown, or revoked. |
| `403` | `API key is not scoped to a project`, `Project unavailable`, or `API key is missing the required scope '<scope>'`. |
| `429` | The project's `api_rpm` ceiling was exceeded. Honour `Retry-After`; `X-RateLimit-*` headers are on every response. |

Scopes used on this page: `provider_account:read`, `provider_account:write`. Mint a key with only these.

### Account door errors

| Status | Cause |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. Refresh once, then re-authenticate. |
| `403` | The account is suspended or its email is not verified. |

## Operations — project API key

Call these from your own server with a project key. Never from a browser or a mobile bundle.

### GET `/v2/provider-accounts`

List the project's saved provider accounts with their verification status. Never credential material.

| Detail | Value |
| --- | --- |
| Door | Project API key — `X-Api-Key: $DVAARIK_API_KEY` |
| Scope | `provider_account:read` |
| Operation | `list_provider_accounts_v2_provider_accounts_get` |
| Success | `200` `ProviderAccountInfo[]` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `include_disconnected` | query | boolean | no |  |
| `limit` | query | integer | no |  |
| `offset` | query | integer | no |  |

#### `ProviderAccountInfo`

Everything safe to return for a saved account. NO credential material — `hint` is a masked fingerprint and is the only echo of the secret.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `capabilities` | string[] | yes |  |
| `created_at` | date-time | yes |  |
| `hint` | object | yes |  |
| `id` | uuid | yes |  |
| `key_version` | integer | yes |  |
| `last_error` | string or null | yes |  |
| `name` | string | yes |  |
| `project_id` | uuid | yes |  |
| `provider` | string | yes |  |
| `revision` | integer | yes |  |
| `status` | string | yes |  |
| `updated_at` | date-time | yes |  |
| `verified_at` | date-time or null | yes |  |

| Error | When |
| --- | --- |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the machine-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X GET "https://api.developers.dvaarik.com/v2/provider-accounts?limit=20" \
  -H "X-Api-Key: $DVAARIK_API_KEY"
```

### POST `/v2/provider-accounts`

Save an AI credential for this project. It is encrypted at rest and the account starts `configured` — saved, not yet usable.

| Detail | Value |
| --- | --- |
| Door | Project API key — `X-Api-Key: $DVAARIK_API_KEY` |
| Scope | `provider_account:write` |
| Operation | `save_provider_account_v2_provider_accounts_post` |
| Success | `201` `ProviderAccountInfo` |

Request body: `SaveProviderAccountRequest`.

#### `SaveProviderAccountRequest`

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `credentials` | object | yes |  |
| `name` | string | yes |  |
| `provider` | string | yes |  |

| Error | When |
| --- | --- |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the machine-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X POST "https://api.developers.dvaarik.com/v2/provider-accounts" \
  -H "X-Api-Key: $DVAARIK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "credentials": {
      "<credential field from the catalogue>": "<secret>"
    },
    "name": "Production account",
    "provider": "<provider id from the catalogue>"
  }'
```

### DELETE `/v2/provider-accounts/{account_id}`

Disconnect the account and forget its credential.

| Detail | Value |
| --- | --- |
| Door | Project API key — `X-Api-Key: $DVAARIK_API_KEY` |
| Scope | `provider_account:write` |
| Operation | `disconnect_provider_account_v2_provider_accounts__account_id__delete` |
| Success | `200` `ProviderAccountInfo` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `account_id` | path | uuid | yes |  |

| Error | When |
| --- | --- |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the machine-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X DELETE "https://api.developers.dvaarik.com/v2/provider-accounts/$ACCOUNT_ID" \
  -H "X-Api-Key: $DVAARIK_API_KEY"
```

### GET `/v2/provider-accounts/{account_id}`

Read one saved account: capabilities, masked hint, status, last error and verification time.

| Detail | Value |
| --- | --- |
| Door | Project API key — `X-Api-Key: $DVAARIK_API_KEY` |
| Scope | `provider_account:read` |
| Operation | `get_provider_account_v2_provider_accounts__account_id__get` |
| Success | `200` `ProviderAccountInfo` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `account_id` | path | uuid | yes |  |

| Error | When |
| --- | --- |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the machine-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X GET "https://api.developers.dvaarik.com/v2/provider-accounts/$ACCOUNT_ID" \
  -H "X-Api-Key: $DVAARIK_API_KEY"
```

### POST `/v2/provider-accounts/{account_id}/probe`

Ask the provider to confirm the stored credential. Confirmed becomes `verified`; rejected becomes `invalid`; a transient provider failure stays `configured`, so an uncertain probe never admits a call.

| Detail | Value |
| --- | --- |
| Door | Project API key — `X-Api-Key: $DVAARIK_API_KEY` |
| Scope | `provider_account:write` |
| Operation | `probe_provider_account_v2_provider_accounts__account_id__probe_post` |
| Success | `200` `ProviderAccountInfo` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `account_id` | path | uuid | yes |  |

| Error | When |
| --- | --- |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the machine-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X POST "https://api.developers.dvaarik.com/v2/provider-accounts/$ACCOUNT_ID/probe" \
  -H "X-Api-Key: $DVAARIK_API_KEY"
```

### POST `/v2/provider-accounts/{account_id}/rotate`

Re-encrypt the stored secret under the current keyring key. The credential itself does not change.

| Detail | Value |
| --- | --- |
| Door | Project API key — `X-Api-Key: $DVAARIK_API_KEY` |
| Scope | `provider_account:write` |
| Operation | `rotate_provider_account_v2_provider_accounts__account_id__rotate_post` |
| Success | `200` `ProviderAccountInfo` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `account_id` | path | uuid | yes |  |

| Error | When |
| --- | --- |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the machine-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X POST "https://api.developers.dvaarik.com/v2/provider-accounts/$ACCOUNT_ID/rotate" \
  -H "X-Api-Key: $DVAARIK_API_KEY"
```

## Operations — account session

The same resource through the console door. These are what the web console calls; both doors return the same shapes.

### GET `/console/v2/projects/{project_id}/provider-accounts`

List the project's saved provider accounts with their verification status. Never credential material.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `consoleListProviderAccounts` |
| Success | `200` `ProviderAccountInfo[]` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `project_id` | path | uuid | yes |  |
| `include_disconnected` | query | boolean | no |  |
| `limit` | query | integer | no |  |
| `offset` | query | integer | no |  |

| Error | When |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. |
| `403` | The account is suspended or its email is not verified. |
| `404` | The project or the addressed resource does not exist for this owner. A resource owned by another developer is deliberately indistinguishable from one that never existed. |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X GET "https://api.developers.dvaarik.com/console/v2/projects/$PROJECT_ID/provider-accounts?limit=20" \
  -H "Authorization: Bearer $DVAARIK_TOKEN"
```

### POST `/console/v2/projects/{project_id}/provider-accounts`

Save an AI credential for this project. It is encrypted at rest and the account starts `configured` — saved, not yet usable.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `consoleSaveProviderAccount` |
| Success | `201` `ProviderAccountInfo` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `project_id` | path | uuid | yes |  |

Request body: `SaveProviderAccountRequest`.

| Error | When |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. |
| `403` | The account is suspended or its email is not verified. |
| `404` | The project or the addressed resource does not exist for this owner. A resource owned by another developer is deliberately indistinguishable from one that never existed. |
| `409` | The project or resource is in a state that refuses this change. |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X POST "https://api.developers.dvaarik.com/console/v2/projects/$PROJECT_ID/provider-accounts" \
  -H "Authorization: Bearer $DVAARIK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "credentials": {
      "<credential field from the catalogue>": "<secret>"
    },
    "name": "Production account",
    "provider": "<provider id from the catalogue>"
  }'
```

### DELETE `/console/v2/projects/{project_id}/provider-accounts/{account_id}`

Disconnect the account and forget its credential.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `consoleDisconnectProviderAccount` |
| Success | `200` `ProviderAccountInfo` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `account_id` | path | uuid | yes |  |
| `project_id` | path | uuid | yes |  |

| Error | When |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. |
| `403` | The account is suspended or its email is not verified. |
| `404` | The project or the addressed resource does not exist for this owner. A resource owned by another developer is deliberately indistinguishable from one that never existed. |
| `409` | The project or resource is in a state that refuses this change. |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X DELETE "https://api.developers.dvaarik.com/console/v2/projects/$PROJECT_ID/provider-accounts/$ACCOUNT_ID" \
  -H "Authorization: Bearer $DVAARIK_TOKEN"
```

### GET `/console/v2/projects/{project_id}/provider-accounts/{account_id}`

Read one saved account: capabilities, masked hint, status, last error and verification time.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `consoleGetProviderAccount` |
| Success | `200` `ProviderAccountInfo` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `account_id` | path | uuid | yes |  |
| `project_id` | path | uuid | yes |  |

| Error | When |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. |
| `403` | The account is suspended or its email is not verified. |
| `404` | The project or the addressed resource does not exist for this owner. A resource owned by another developer is deliberately indistinguishable from one that never existed. |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X GET "https://api.developers.dvaarik.com/console/v2/projects/$PROJECT_ID/provider-accounts/$ACCOUNT_ID" \
  -H "Authorization: Bearer $DVAARIK_TOKEN"
```

### POST `/console/v2/projects/{project_id}/provider-accounts/{account_id}/probe`

Ask the provider to confirm the stored credential. Confirmed becomes `verified`; rejected becomes `invalid`; a transient provider failure stays `configured`, so an uncertain probe never admits a call.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `consoleProbeProviderAccount` |
| Success | `200` `ProviderAccountInfo` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `account_id` | path | uuid | yes |  |
| `project_id` | path | uuid | yes |  |

| Error | When |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. |
| `403` | The account is suspended or its email is not verified. |
| `404` | The project or the addressed resource does not exist for this owner. A resource owned by another developer is deliberately indistinguishable from one that never existed. |
| `409` | The project or resource is in a state that refuses this change. |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X POST "https://api.developers.dvaarik.com/console/v2/projects/$PROJECT_ID/provider-accounts/$ACCOUNT_ID/probe" \
  -H "Authorization: Bearer $DVAARIK_TOKEN"
```

### POST `/console/v2/projects/{project_id}/provider-accounts/{account_id}/rotate`

Re-encrypt the stored secret under the current keyring key. The credential itself does not change.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `consoleRotateProviderAccount` |
| Success | `200` `ProviderAccountInfo` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `account_id` | path | uuid | yes |  |
| `project_id` | path | uuid | yes |  |

| Error | When |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. |
| `403` | The account is suspended or its email is not verified. |
| `404` | The project or the addressed resource does not exist for this owner. A resource owned by another developer is deliberately indistinguishable from one that never existed. |
| `409` | The project or resource is in a state that refuses this change. |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X POST "https://api.developers.dvaarik.com/console/v2/projects/$PROJECT_ID/provider-accounts/$ACCOUNT_ID/rotate" \
  -H "Authorization: Bearer $DVAARIK_TOKEN"
```

### GET `/v2/provider-catalog`

The connectable AI providers with their capabilities, credential fields and model ids. Render your form from this, never from a list you copied.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `get_provider_catalog_v2_provider_catalog_get` |
| Success | `200` `any` |

```bash
curl -sS -X GET "https://api.developers.dvaarik.com/v2/provider-catalog" \
  -H "Authorization: Bearer $DVAARIK_TOKEN"
```

# Agents API

## What this covers

An agent is the frozen configuration a call runs against: pipeline, prompt, language, limits and webhook. Sessions record the agent version they ran, so editing an agent never rewrites history.

Every example below runs as written once these are exported:

```bash
export DVAARIK_API_KEY="dvk_..."        # project API key, server side only
export DVAARIK_TOKEN="<account access token>"   # console session, from POST /console/auth/login
export AGENT_ID="<uuid>"
export PROJECT_ID="<uuid>"
```

A `<value>` inside a JSON body is a value you supply. Responses are JSON; every error body is `{ "detail": … }`. Each schema is expanded once per page, where it first appears.

### Machine door errors

Raised by the project-key dependency before any handler on this page runs.

| Status | Cause |
| --- | --- |
| `401` | `Missing API key` or `Invalid API key` — the `X-Api-Key` header is absent, unknown, or revoked. |
| `403` | `API key is not scoped to a project`, `Project unavailable`, or `API key is missing the required scope '<scope>'`. |
| `429` | The project's `api_rpm` ceiling was exceeded. Honour `Retry-After`; `X-RateLimit-*` headers are on every response. |

Scopes used on this page: `voice:read`, `voice:write`. Mint a key with only these.

### Account door errors

| Status | Cause |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. Refresh once, then re-authenticate. |
| `403` | The account is suspended or its email is not verified. |

## Operations — project API key

Call these from your own server with a project key. Never from a browser or a mobile bundle.

### GET `/v2/agents`

List the project's agents.

| Detail | Value |
| --- | --- |
| Door | Project API key — `X-Api-Key: $DVAARIK_API_KEY` |
| Scope | `voice:read` |
| Operation | `list_agents_v2_agents_get` |
| Success | `200` `V2AgentInfo[]` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `limit` | query | integer | no |  |
| `offset` | query | integer | no |  |
| `include_inactive` | query | boolean | no |  |

#### `V2AgentInfo`

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `ambience_level` | integer or null | yes |  |
| `ambience_sound` | string or null | yes |  |
| `backchannel` | boolean | yes |  |
| `can_send_dtmf` | boolean | yes |  |
| `created_at` | date-time | yes |  |
| `dead_air_seconds` | integer or null | yes |  |
| `display_name` | string or null | yes |  |
| `greeting` | string or null | yes |  |
| `has_webhook_secret` | boolean | no | default false |
| `id` | uuid | yes |  |
| `idle_hangup_seconds` | integer or null | yes |  |
| `idle_warn_seconds` | integer or null | yes |  |
| `insight_config` | InsightConfigBody or null | no |  |
| `is_active` | boolean | yes |  |
| `language` | string | yes |  |
| `max_chars_per_min` | integer or null | yes |  |
| `max_duration_seconds` | integer or null | yes |  |
| `name` | string | yes |  |
| `pipeline_config` | object | yes |  |
| `pipeline_mode` | string | yes |  |
| `project_id` | uuid | yes |  |
| `prompt` | string | yes |  |
| `record_calls` | boolean | yes |  |
| `retention_days` | integer or null | yes |  |
| `store_transcript` | boolean | yes |  |
| `tool_webhook_url` | string or null | yes |  |
| `tools` | any[] | yes |  |
| `updated_at` | date-time | yes |  |
| `variables` | any[] | yes |  |
| `version` | integer | yes |  |
| `webhook_include_recording_url` | boolean | yes |  |
| `webhook_include_transcript` | boolean | yes |  |
| `webhook_url` | string or null | yes |  |

| Error | When |
| --- | --- |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the machine-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X GET "https://api.developers.dvaarik.com/v2/agents?limit=20" \
  -H "X-Api-Key: $DVAARIK_API_KEY"
```

### POST `/v2/agents`

Create an agent. The response carries its webhook signing secret once.

| Detail | Value |
| --- | --- |
| Door | Project API key — `X-Api-Key: $DVAARIK_API_KEY` |
| Scope | `voice:write` |
| Operation | `create_agent_v2_agents_post` |
| Success | `201` `V2CreatedAgentResponse` |

Request body: `V2AgentCreate`.

#### `V2AgentCreate`

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `ambience_level` | integer or null | no |  |
| `ambience_sound` | string or null | no |  |
| `backchannel` | boolean | no | default true |
| `can_send_dtmf` | boolean | no | default false |
| `dead_air_seconds` | integer or null | no |  |
| `display_name` | string or null | no |  |
| `greeting` | string or null | no |  |
| `idle_hangup_seconds` | integer or null | no |  |
| `idle_warn_seconds` | integer or null | no |  |
| `insight_config` | InsightConfigBody or null | no |  |
| `language` | string | no | default "en-IN" |
| `max_chars_per_min` | integer or null | no |  |
| `max_duration_seconds` | integer or null | no |  |
| `name` | string | yes |  |
| `pipeline_config` | object | yes |  |
| `pipeline_mode` | string | yes |  |
| `prompt` | string | yes |  |
| `record_calls` | boolean | no | default false |
| `retention_days` | integer or null | no |  |
| `store_transcript` | boolean | no | default false |
| `tool_webhook_url` | string or null | no |  |
| `tools` | object[] | no |  |
| `variables` | object[] | no |  |
| `webhook_include_recording_url` | boolean | no | default false |
| `webhook_include_transcript` | boolean | no | default false |
| `webhook_url` | string or null | no |  |

#### `V2CreatedAgentResponse`

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `agent` | V2AgentInfo | yes |  |
| `webhook_secret` | string | yes |  |

| Error | When |
| --- | --- |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the machine-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X POST "https://api.developers.dvaarik.com/v2/agents" \
  -H "X-Api-Key: $DVAARIK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "support",
    "pipeline_config": {
      "stt": {
        "account_id": "<provider account uuid>",
        "model": "<model id from the catalogue>"
      },
      "llm": {
        "account_id": "<provider account uuid>",
        "model": "<model id from the catalogue>"
      },
      "tts": {
        "account_id": "<provider account uuid>",
        "model": "<model id from the catalogue>",
        "voice": "<provider voice id>"
      }
    },
    "pipeline_mode": "cascade",
    "prompt": "You are the receptionist for {{company}}. Answer questions and book visits.",
    "greeting": "Hello, this is support.",
    "language": "en-IN"
  }'
```

### DELETE `/v2/agents/{agent_id}`

Deactivate the agent. Past sessions keep the agent id and version they ran; numbers bound to it stop answering.

| Detail | Value |
| --- | --- |
| Door | Project API key — `X-Api-Key: $DVAARIK_API_KEY` |
| Scope | `voice:write` |
| Operation | `delete_agent_v2_agents__agent_id__delete` |
| Success | `200` `V2AgentDeleted` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `agent_id` | path | uuid | yes |  |

#### `V2AgentDeleted`

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `calls` | integer | yes |  |
| `deleted` | boolean | yes |  |
| `id` | uuid | yes |  |
| `name` | string | yes |  |

| Error | When |
| --- | --- |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the machine-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X DELETE "https://api.developers.dvaarik.com/v2/agents/$AGENT_ID" \
  -H "X-Api-Key: $DVAARIK_API_KEY"
```

### GET `/v2/agents/{agent_id}`

Read one agent, including the pipeline it will run.

| Detail | Value |
| --- | --- |
| Door | Project API key — `X-Api-Key: $DVAARIK_API_KEY` |
| Scope | `voice:read` |
| Operation | `get_agent_v2_agents__agent_id__get` |
| Success | `200` `V2AgentInfo` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `agent_id` | path | uuid | yes |  |

| Error | When |
| --- | --- |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the machine-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X GET "https://api.developers.dvaarik.com/v2/agents/$AGENT_ID" \
  -H "X-Api-Key: $DVAARIK_API_KEY"
```

### PATCH `/v2/agents/{agent_id}`

Update an agent. Only the fields you send change, and the version increments.

| Detail | Value |
| --- | --- |
| Door | Project API key — `X-Api-Key: $DVAARIK_API_KEY` |
| Scope | `voice:write` |
| Operation | `update_agent_v2_agents__agent_id__patch` |
| Success | `200` `V2AgentInfo` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `agent_id` | path | uuid | yes |  |

Request body: `V2AgentUpdate`.

#### `V2AgentUpdate`

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `ambience_level` | integer or null | no |  |
| `ambience_sound` | string or null | no |  |
| `backchannel` | boolean or null | no |  |
| `can_send_dtmf` | boolean or null | no |  |
| `dead_air_seconds` | integer or null | no |  |
| `display_name` | string or null | no |  |
| `greeting` | string or null | no |  |
| `idle_hangup_seconds` | integer or null | no |  |
| `idle_warn_seconds` | integer or null | no |  |
| `insight_config` | InsightConfigBody or null | no |  |
| `is_active` | boolean or null | no |  |
| `language` | string or null | no |  |
| `max_chars_per_min` | integer or null | no |  |
| `max_duration_seconds` | integer or null | no |  |
| `name` | string or null | no |  |
| `pipeline_config` | object or null | no |  |
| `pipeline_mode` | string or null | no |  |
| `prompt` | string or null | no |  |
| `record_calls` | boolean or null | no |  |
| `retention_days` | integer or null | no |  |
| `store_transcript` | boolean or null | no |  |
| `tool_webhook_url` | string or null | no |  |
| `tools` | object[] or null | no |  |
| `variables` | object[] or null | no |  |
| `webhook_include_recording_url` | boolean or null | no |  |
| `webhook_include_transcript` | boolean or null | no |  |
| `webhook_url` | string or null | no |  |

| Error | When |
| --- | --- |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the machine-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X PATCH "https://api.developers.dvaarik.com/v2/agents/$AGENT_ID" \
  -H "X-Api-Key: $DVAARIK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "greeting": "Hello, this is support. How can I help?",
    "is_active": true
  }'
```

### POST `/v2/agents/{agent_id}/rotate-secret`

Issue a new webhook signing secret for this agent and return it once.

| Detail | Value |
| --- | --- |
| Door | Project API key — `X-Api-Key: $DVAARIK_API_KEY` |
| Scope | `voice:write` |
| Operation | `rotate_secret_v2_agents__agent_id__rotate_secret_post` |
| Success | `200` `V2CreatedAgentResponse` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `agent_id` | path | uuid | yes |  |

| Error | When |
| --- | --- |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the machine-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X POST "https://api.developers.dvaarik.com/v2/agents/$AGENT_ID/rotate-secret" \
  -H "X-Api-Key: $DVAARIK_API_KEY"
```

## Operations — account session

The same resource through the console door. These are what the web console calls; both doors return the same shapes.

### GET `/console/v2/projects/{project_id}/agents`

List the project's agents.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `consoleListAgents` |
| Success | `200` `V2AgentInfo[]` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `project_id` | path | uuid | yes |  |
| `limit` | query | integer | no |  |
| `offset` | query | integer | no |  |
| `include_inactive` | query | boolean | no |  |

| Error | When |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. |
| `403` | The account is suspended or its email is not verified. |
| `404` | The project or the addressed resource does not exist for this owner. A resource owned by another developer is deliberately indistinguishable from one that never existed. |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X GET "https://api.developers.dvaarik.com/console/v2/projects/$PROJECT_ID/agents?limit=20" \
  -H "Authorization: Bearer $DVAARIK_TOKEN"
```

### POST `/console/v2/projects/{project_id}/agents`

Create an agent. The response carries its webhook signing secret once.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `consoleCreateAgent` |
| Success | `201` `V2CreatedAgentResponse` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `project_id` | path | uuid | yes |  |

Request body: `V2AgentCreate`.

| Error | When |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. |
| `403` | The account is suspended or its email is not verified. |
| `404` | The project or the addressed resource does not exist for this owner. A resource owned by another developer is deliberately indistinguishable from one that never existed. |
| `409` | The project or resource is in a state that refuses this change. |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X POST "https://api.developers.dvaarik.com/console/v2/projects/$PROJECT_ID/agents" \
  -H "Authorization: Bearer $DVAARIK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "support",
    "pipeline_config": {
      "stt": {
        "account_id": "<provider account uuid>",
        "model": "<model id from the catalogue>"
      },
      "llm": {
        "account_id": "<provider account uuid>",
        "model": "<model id from the catalogue>"
      },
      "tts": {
        "account_id": "<provider account uuid>",
        "model": "<model id from the catalogue>",
        "voice": "<provider voice id>"
      }
    },
    "pipeline_mode": "cascade",
    "prompt": "You are the receptionist for {{company}}. Answer questions and book visits.",
    "greeting": "Hello, this is support.",
    "language": "en-IN"
  }'
```

### DELETE `/console/v2/projects/{project_id}/agents/{agent_id}`

Deactivate the agent. Past sessions keep the agent id and version they ran; numbers bound to it stop answering.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `consoleDeleteAgent` |
| Success | `200` `V2AgentDeleted` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `agent_id` | path | uuid | yes |  |
| `project_id` | path | uuid | yes |  |

| Error | When |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. |
| `403` | The account is suspended or its email is not verified. |
| `404` | The project or the addressed resource does not exist for this owner. A resource owned by another developer is deliberately indistinguishable from one that never existed. |
| `409` | The project or resource is in a state that refuses this change. |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X DELETE "https://api.developers.dvaarik.com/console/v2/projects/$PROJECT_ID/agents/$AGENT_ID" \
  -H "Authorization: Bearer $DVAARIK_TOKEN"
```

### GET `/console/v2/projects/{project_id}/agents/{agent_id}`

Read one agent, including the pipeline it will run.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `consoleGetAgent` |
| Success | `200` `V2AgentInfo` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `agent_id` | path | uuid | yes |  |
| `project_id` | path | uuid | yes |  |

| Error | When |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. |
| `403` | The account is suspended or its email is not verified. |
| `404` | The project or the addressed resource does not exist for this owner. A resource owned by another developer is deliberately indistinguishable from one that never existed. |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X GET "https://api.developers.dvaarik.com/console/v2/projects/$PROJECT_ID/agents/$AGENT_ID" \
  -H "Authorization: Bearer $DVAARIK_TOKEN"
```

### PATCH `/console/v2/projects/{project_id}/agents/{agent_id}`

Update an agent. Only the fields you send change, and the version increments.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `consoleUpdateAgent` |
| Success | `200` `V2AgentInfo` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `agent_id` | path | uuid | yes |  |
| `project_id` | path | uuid | yes |  |

Request body: `V2AgentUpdate`.

| Error | When |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. |
| `403` | The account is suspended or its email is not verified. |
| `404` | The project or the addressed resource does not exist for this owner. A resource owned by another developer is deliberately indistinguishable from one that never existed. |
| `409` | The project or resource is in a state that refuses this change. |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X PATCH "https://api.developers.dvaarik.com/console/v2/projects/$PROJECT_ID/agents/$AGENT_ID" \
  -H "Authorization: Bearer $DVAARIK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "greeting": "Hello, this is support. How can I help?",
    "is_active": true
  }'
```

### POST `/console/v2/projects/{project_id}/agents/{agent_id}/rotate-secret`

Issue a new webhook signing secret for this agent and return it once.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `consoleRotateAgentSecret` |
| Success | `200` `V2CreatedAgentResponse` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `agent_id` | path | uuid | yes |  |
| `project_id` | path | uuid | yes |  |

| Error | When |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. |
| `403` | The account is suspended or its email is not verified. |
| `404` | The project or the addressed resource does not exist for this owner. A resource owned by another developer is deliberately indistinguishable from one that never existed. |
| `409` | The project or resource is in a state that refuses this change. |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X POST "https://api.developers.dvaarik.com/console/v2/projects/$PROJECT_ID/agents/$AGENT_ID/rotate-secret" \
  -H "Authorization: Bearer $DVAARIK_TOKEN"
```

# Voice sessions

## What this covers

One session is one call. Create it to get a single-use WebSocket URL for browser audio, or dial out on your own carrier line. Billing is settled from connected seconds when the session ends.

Every example below runs as written once these are exported:

```bash
export DVAARIK_API_KEY="dvk_..."        # project API key, server side only
export DVAARIK_TOKEN="<account access token>"   # console session, from POST /console/auth/login
export PROJECT_ID="<uuid>"
export SESSION_ID="<uuid>"
```

A `<value>` inside a JSON body is a value you supply. Responses are JSON; every error body is `{ "detail": … }`. Each schema is expanded once per page, where it first appears.

### Machine door errors

Raised by the project-key dependency before any handler on this page runs.

| Status | Cause |
| --- | --- |
| `401` | `Missing API key` or `Invalid API key` — the `X-Api-Key` header is absent, unknown, or revoked. |
| `403` | `API key is not scoped to a project`, `Project unavailable`, or `API key is missing the required scope '<scope>'`. |
| `429` | The project's `api_rpm` ceiling was exceeded. Honour `Retry-After`; `X-RateLimit-*` headers are on every response. |

Scopes used on this page: `voice:read`, `voice:write`. Mint a key with only these.

### Account door errors

| Status | Cause |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. Refresh once, then re-authenticate. |
| `403` | The account is suspended or its email is not verified. |

## Operations — project API key

Call these from your own server with a project key. Never from a browser or a mobile bundle.

### GET `/v2/voice/sessions`

List sessions newest first, filtered by status, agent or date range.

| Detail | Value |
| --- | --- |
| Door | Project API key — `X-Api-Key: $DVAARIK_API_KEY` |
| Scope | `voice:read` |
| Operation | `list_voice_sessions_v2_voice_sessions_get` |
| Success | `200` `VoiceSessionInfo[]` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `limit` | query | integer | no |  |
| `offset` | query | integer | no |  |
| `status` | query | string or null | no |  |
| `agent_id` | query | uuid or null | no |  |
| `from` | query | date-time or null | no |  |
| `to` | query | date-time or null | no |  |
| `has_insights` | query | boolean or null | no | true for sessions whose post-call analysis produced a result; false for every other session. |

#### `VoiceSessionInfo`

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `agent_id` | uuid or null | yes |  |
| `agent_version` | integer or null | yes |  |
| `connected_seconds` | integer | yes |  |
| `cost_nano_usd` | string | yes |  |
| `created_at` | date-time | yes |  |
| `currency` | string | yes |  |
| `direction` | string | yes |  |
| `end_reason` | string or null | yes |  |
| `ended_at` | date-time or null | yes |  |
| `hold_nano_usd` | string | yes |  |
| `id` | uuid | yes |  |
| `insights` | CallInsightsInfo or null | no |  |
| `language` | string | yes |  |
| `max_duration_seconds` | integer | yes |  |
| `media_connected_at` | date-time or null | yes |  |
| `media_disconnected_at` | date-time or null | yes |  |
| `metadata` | object or null | yes |  |
| `money_scale` | integer | yes |  |
| `pipeline_mode` | string | yes |  |
| `pipeline_snapshot` | object | yes |  |
| `project_id` | uuid | yes |  |
| `project_revision` | integer | yes |  |
| `rate_nano_usd_per_min` | string | yes |  |
| `rate_version` | string | yes |  |
| `record` | boolean | yes |  |
| `recording_ref` | object or null | no | Where this call's audio is, or null when there is none. The audio lives in your own carrier account and is fetched with your own carrier credentials, not ours — Dvaarik does not store call recordings. The block is {provider, reference_id, url, fetched_from}: `fetched_from` is "carrier" when your carrier holds the recording (a null `reference_id` there means the carrier accepted the request and will name the recording on its own webhook), and "unavailable" with a `detail` sentence when the carrier could not be asked at all. Browser and bring-your-own-socket sessions have no carrier, so they have no recording and this is always null for them — there is no fallback, because a fallback would mean us storing the audio again. |
| `sample_rate_in` | integer | yes |  |
| `sample_rate_out` | integer | yes |  |
| `started_at` | date-time or null | yes |  |
| `status` | string | yes |  |
| `store_transcript` | boolean | yes |  |

| Error | When |
| --- | --- |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the machine-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X GET "https://api.developers.dvaarik.com/v2/voice/sessions?limit=20" \
  -H "X-Api-Key: $DVAARIK_API_KEY"
```

### POST `/v2/voice/sessions`

Admit a media session and return a single-use `ws_url` valid for `expires_in_seconds`. Hand only that URL to the client that streams audio.

| Detail | Value |
| --- | --- |
| Door | Project API key — `X-Api-Key: $DVAARIK_API_KEY` |
| Scope | `voice:write` |
| Operation | `create_voice_session_v2_voice_sessions_post` |
| Success | `201` `CreateVoiceSessionResponse` |

Request body: `CreateVoiceSessionRequest`.

#### `CreateVoiceSessionRequest`

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `agent_id` | uuid | yes |  |
| `max_duration_seconds` | integer or null | no |  |
| `metadata` | object or null | no |  |
| `record` | boolean or null | no |  |
| `sample_rate_in` | integer | no | default 16000 |
| `sample_rate_out` | integer | no | default 24000 |
| `store_transcript` | boolean or null | no |  |
| `variables` | object<string> | no |  |

#### `CreateVoiceSessionResponse`

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `expires_in_seconds` | integer | yes |  |
| `session` | VoiceSessionInfo | yes |  |
| `ws_url` | string | yes |  |

| Error | When |
| --- | --- |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the machine-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X POST "https://api.developers.dvaarik.com/v2/voice/sessions" \
  -H "X-Api-Key: $DVAARIK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "<agent uuid>",
    "sample_rate_in": 16000,
    "sample_rate_out": 24000,
    "variables": {
      "company": "Northline Dental"
    }
  }'
```

### GET `/v2/voice/sessions/{session_id}`

Read one session with its frozen pipeline snapshot, connected seconds and settled cost.

| Detail | Value |
| --- | --- |
| Door | Project API key — `X-Api-Key: $DVAARIK_API_KEY` |
| Scope | `voice:read` |
| Operation | `get_voice_session_v2_voice_sessions__session_id__get` |
| Success | `200` `VoiceSessionInfo` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `session_id` | path | uuid | yes |  |

| Error | When |
| --- | --- |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the machine-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X GET "https://api.developers.dvaarik.com/v2/voice/sessions/$SESSION_ID" \
  -H "X-Api-Key: $DVAARIK_API_KEY"
```

### POST `/v2/voice/sessions/outbound`

Queue one outbound call from a number on your own verified carrier connection. Send an `Idempotency-Key`; a repeat returns the first call with `duplicate: true`.

| Detail | Value |
| --- | --- |
| Door | Project API key — `X-Api-Key: $DVAARIK_API_KEY` |
| Scope | `voice:write` |
| Operation | `create_outbound_voice_session_v2_voice_sessions_outbound_post` |
| Success | `202` `CreateOutboundVoiceSessionResponse` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` | header | string or null | no |  |

Request body: `CreateOutboundVoiceSessionRequest`.

#### `CreateOutboundVoiceSessionRequest`

One queued call through a project-owned carrier connection and agent.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `agent_id` | uuid | yes |  |
| `from` | string | yes |  |
| `max_duration_seconds` | integer or null | no |  |
| `metadata` | object or null | no |  |
| `record` | boolean or null | no |  |
| `store_transcript` | boolean or null | no |  |
| `to` | string | yes |  |
| `variables` | object<string> | no |  |

#### `CreateOutboundVoiceSessionResponse`

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `duplicate` | boolean | no | default false |
| `session` | VoiceSessionInfo | yes |  |

| Error | When |
| --- | --- |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the machine-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X POST "https://api.developers.dvaarik.com/v2/voice/sessions/outbound" \
  -H "X-Api-Key: $DVAARIK_API_KEY" \
  -H "Idempotency-Key: <unique per call attempt>" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "<agent uuid>",
    "from": "+15550000001",
    "to": "+15550000002"
  }'
```

## Operations — account session

The same resource through the console door. These are what the web console calls; both doors return the same shapes.

### GET `/console/v2/projects/{project_id}/voice/sessions`

List sessions newest first, filtered by status, agent or date range.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `consoleListVoiceSessions` |
| Success | `200` `VoiceSessionInfo[]` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `project_id` | path | uuid | yes |  |
| `limit` | query | integer | no |  |
| `offset` | query | integer | no |  |
| `status` | query | string or null | no |  |
| `agent_id` | query | uuid or null | no |  |
| `from` | query | date-time or null | no |  |
| `to` | query | date-time or null | no |  |
| `has_insights` | query | boolean or null | no | true for sessions whose post-call analysis produced a result; false for every other session. |

| Error | When |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. |
| `403` | The account is suspended or its email is not verified. |
| `404` | The project or the addressed resource does not exist for this owner. A resource owned by another developer is deliberately indistinguishable from one that never existed. |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X GET "https://api.developers.dvaarik.com/console/v2/projects/$PROJECT_ID/voice/sessions?limit=20" \
  -H "Authorization: Bearer $DVAARIK_TOKEN"
```

### POST `/console/v2/projects/{project_id}/voice/sessions`

Admit a media session and return a single-use `ws_url` valid for `expires_in_seconds`. Hand only that URL to the client that streams audio.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `consoleCreateVoiceSession` |
| Success | `201` `CreateVoiceSessionResponse` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `project_id` | path | uuid | yes |  |

Request body: `CreateVoiceSessionRequest`.

| Error | When |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. |
| `402` | Wallet balance cannot cover this session's maximum duration. |
| `403` | The account is suspended or its email is not verified. |
| `404` | The project or the addressed resource does not exist for this owner. A resource owned by another developer is deliberately indistinguishable from one that never existed. |
| `409` | The project or resource is in a state that refuses this change. |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |
| `503` | BYOK provider accounts are unverified/unavailable, or the service is draining. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X POST "https://api.developers.dvaarik.com/console/v2/projects/$PROJECT_ID/voice/sessions" \
  -H "Authorization: Bearer $DVAARIK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "<agent uuid>",
    "sample_rate_in": 16000,
    "sample_rate_out": 24000,
    "variables": {
      "company": "Northline Dental"
    }
  }'
```

### GET `/console/v2/projects/{project_id}/voice/sessions/{session_id}`

Read one session with its frozen pipeline snapshot, connected seconds and settled cost.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `consoleGetVoiceSession` |
| Success | `200` `VoiceSessionInfo` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `session_id` | path | uuid | yes |  |
| `project_id` | path | uuid | yes |  |

| Error | When |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. |
| `403` | The account is suspended or its email is not verified. |
| `404` | The project or the addressed resource does not exist for this owner. A resource owned by another developer is deliberately indistinguishable from one that never existed. |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X GET "https://api.developers.dvaarik.com/console/v2/projects/$PROJECT_ID/voice/sessions/$SESSION_ID" \
  -H "Authorization: Bearer $DVAARIK_TOKEN"
```

### POST `/console/v2/projects/{project_id}/voice/sessions/outbound`

Queue one outbound call from a number on your own verified carrier connection. Send an `Idempotency-Key`; a repeat returns the first call with `duplicate: true`.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `consoleCreateOutboundVoiceSession` |
| Success | `202` `CreateOutboundVoiceSessionResponse` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `project_id` | path | uuid | yes |  |
| `Idempotency-Key` | header | string or null | no |  |

Request body: `CreateOutboundVoiceSessionRequest`.

| Error | When |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. |
| `402` | Wallet balance cannot cover this call's maximum duration. |
| `403` | The account is suspended or its email is not verified. |
| `404` | The project or the addressed resource does not exist for this owner. A resource owned by another developer is deliberately indistinguishable from one that never existed. |
| `409` | The project or resource is in a state that refuses this change. |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |
| `503` | The carrier connection or BYOK pipeline is unavailable. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X POST "https://api.developers.dvaarik.com/console/v2/projects/$PROJECT_ID/voice/sessions/outbound" \
  -H "Authorization: Bearer $DVAARIK_TOKEN" \
  -H "Idempotency-Key: <unique per call attempt>" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "<agent uuid>",
    "from": "+15550000001",
    "to": "+15550000002"
  }'
```

# Carriers and numbers

## What this covers

Bring your own telephony account. Connect it with your carrier credentials, import or attach the numbers you own, and point each number at the URL the API hands back.

Every example below runs as written once these are exported:

```bash
export DVAARIK_API_KEY="dvk_..."        # project API key, server side only
export DVAARIK_TOKEN="<account access token>"   # console session, from POST /console/auth/login
export CONNECTION_ID="<uuid>"
export NUMBER_ID="<uuid>"
export PROJECT_ID="<uuid>"
```

A `<value>` inside a JSON body is a value you supply. Responses are JSON; every error body is `{ "detail": … }`. Each schema is expanded once per page, where it first appears.

### Machine door errors

Raised by the project-key dependency before any handler on this page runs.

| Status | Cause |
| --- | --- |
| `401` | `Missing API key` or `Invalid API key` — the `X-Api-Key` header is absent, unknown, or revoked. |
| `403` | `API key is not scoped to a project`, `Project unavailable`, or `API key is missing the required scope '<scope>'`. |
| `429` | The project's `api_rpm` ceiling was exceeded. Honour `Retry-After`; `X-RateLimit-*` headers are on every response. |

Scopes used on this page: `carrier:read`, `carrier:write`. Mint a key with only these.

### Account door errors

| Status | Cause |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. Refresh once, then re-authenticate. |
| `403` | The account is suspended or its email is not verified. |

## Operations — project API key

Call these from your own server with a project key. Never from a browser or a mobile bundle.

### GET `/v2/carriers`

List the project's carrier connections and their verification state.

| Detail | Value |
| --- | --- |
| Door | Project API key — `X-Api-Key: $DVAARIK_API_KEY` |
| Scope | `carrier:read` |
| Operation | `listCarriers` |
| Success | `200` `V2CarrierInfo[]` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `limit` | query | integer | no |  |
| `offset` | query | integer | no |  |

#### `V2CarrierInfo`

A carrier connection. No credential field exists in this direction.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `created_at` | date-time | yes |  |
| `display_name` | string | yes |  |
| `hint` | object | yes |  |
| `id` | uuid | yes |  |
| `last_error` | string or null | no |  |
| `name` | string | yes |  |
| `number_count` | integer | no | default 0 |
| `project_id` | uuid | yes |  |
| `provider` | string | yes |  |
| `status` | string | yes |  |
| `updated_at` | date-time | yes |  |
| `verified_at` | date-time or null | no |  |

| Error | When |
| --- | --- |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the machine-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X GET "https://api.developers.dvaarik.com/v2/carriers?limit=20" \
  -H "X-Api-Key: $DVAARIK_API_KEY"
```

### POST `/v2/carriers`

Connect a carrier account you already pay for. The credentials are checked against the carrier immediately and a rejected credential is never stored.

| Detail | Value |
| --- | --- |
| Door | Project API key — `X-Api-Key: $DVAARIK_API_KEY` |
| Scope | `carrier:write` |
| Operation | `createCarrier` |
| Success | `201` `V2CarrierInfo` |

Request body: `V2CarrierCreate`.

#### `V2CarrierCreate`

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `credentials` | object<string> | yes |  |
| `name` | string | yes | min length 1; max length 120 |
| `provider` | string | yes | min length 1; max length 32 |

| Error | When |
| --- | --- |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the machine-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X POST "https://api.developers.dvaarik.com/v2/carriers" \
  -H "X-Api-Key: $DVAARIK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "credentials": {
      "<credential field from the catalogue>": "<secret>"
    },
    "name": "Primary line",
    "provider": "<carrier id from the catalogue>"
  }'
```

### DELETE `/v2/carriers/{connection_id}`

Forget the carrier credentials and disable every number on the connection. Your carrier account is never touched.

| Detail | Value |
| --- | --- |
| Door | Project API key — `X-Api-Key: $DVAARIK_API_KEY` |
| Scope | `carrier:write` |
| Operation | `disconnectCarrier` |
| Success | `200` `V2CarrierInfo` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `connection_id` | path | uuid | yes |  |

| Error | When |
| --- | --- |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the machine-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X DELETE "https://api.developers.dvaarik.com/v2/carriers/$CONNECTION_ID" \
  -H "X-Api-Key: $DVAARIK_API_KEY"
```

### GET `/v2/carriers/{connection_id}`

Read one carrier connection.

| Detail | Value |
| --- | --- |
| Door | Project API key — `X-Api-Key: $DVAARIK_API_KEY` |
| Scope | `carrier:read` |
| Operation | `getCarrier` |
| Success | `200` `V2CarrierInfo` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `connection_id` | path | uuid | yes |  |

| Error | When |
| --- | --- |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the machine-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X GET "https://api.developers.dvaarik.com/v2/carriers/$CONNECTION_ID" \
  -H "X-Api-Key: $DVAARIK_API_KEY"
```

### POST `/v2/carriers/{connection_id}/numbers`

Attach one E.164 number by hand, for a carrier that cannot list its numbers.

| Detail | Value |
| --- | --- |
| Door | Project API key — `X-Api-Key: $DVAARIK_API_KEY` |
| Scope | `carrier:write` |
| Operation | `attachCarrierNumber` |
| Success | `201` `V2CarrierNumberInfo` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `connection_id` | path | uuid | yes |  |

Request body: `V2CarrierNumberAttach`.

#### `V2CarrierNumberAttach`

Attach one E.164 the developer's own carrier account holds.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `label` | string or null | no |  |
| `phone_number` | string | yes | min length 7; max length 24 |

#### `V2CarrierNumberInfo`

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `answer_url` | string | no | default "" |
| `connection_id` | uuid | yes |  |
| `created_at` | date-time | yes |  |
| `id` | uuid | yes |  |
| `inbound_agent_id` | uuid or null | no |  |
| `inbound_enabled` | boolean | yes |  |
| `label` | string or null | no |  |
| `outbound_enabled` | boolean | yes |  |
| `phone_number` | string | yes |  |
| `project_id` | uuid | yes |  |
| `provider` | string | yes |  |
| `stream_url` | string | no | default "" |
| `updated_at` | date-time | yes |  |

| Error | When |
| --- | --- |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the machine-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X POST "https://api.developers.dvaarik.com/v2/carriers/$CONNECTION_ID/numbers" \
  -H "X-Api-Key: $DVAARIK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phone_number": "+15550000001",
    "label": "Main line"
  }'
```

### POST `/v2/carriers/{connection_id}/numbers/import`

Pull the numbers this carrier account already holds.

| Detail | Value |
| --- | --- |
| Door | Project API key — `X-Api-Key: $DVAARIK_API_KEY` |
| Scope | `carrier:write` |
| Operation | `importCarrierNumbers` |
| Success | `200` `V2CarrierNumberImportResult` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `connection_id` | path | uuid | yes |  |

#### `V2CarrierNumberImportResult`

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `added` | integer | yes |  |
| `numbers` | V2CarrierNumberInfo[] | yes |  |
| `updated` | integer | yes |  |

| Error | When |
| --- | --- |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the machine-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X POST "https://api.developers.dvaarik.com/v2/carriers/$CONNECTION_ID/numbers/import" \
  -H "X-Api-Key: $DVAARIK_API_KEY"
```

### POST `/v2/carriers/{connection_id}/verify`

Re-check the stored credentials, or replace them when a body is sent — and only if the carrier accepts the replacement.

| Detail | Value |
| --- | --- |
| Door | Project API key — `X-Api-Key: $DVAARIK_API_KEY` |
| Scope | `carrier:write` |
| Operation | `verifyCarrier` |
| Success | `200` `V2CarrierInfo` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `connection_id` | path | uuid | yes |  |

| Error | When |
| --- | --- |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the machine-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X POST "https://api.developers.dvaarik.com/v2/carriers/$CONNECTION_ID/verify" \
  -H "X-Api-Key: $DVAARIK_API_KEY"
```

### GET `/v2/carriers/catalog`

The connectable carriers with the exact credential fields to render and, per carrier, whether it fetches an answer URL or takes a stream URL.

| Detail | Value |
| --- | --- |
| Door | Project API key — `X-Api-Key: $DVAARIK_API_KEY` |
| Scope | `carrier:read` |
| Operation | `listCarrierCatalog` |
| Success | `200` `V2CarrierCatalog` |

#### `V2CarrierCatalog`

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `carriers` | V2CarrierCatalogEntry[] | yes |  |

#### `V2CarrierCatalogEntry`

One connectable carrier: what to type, and what to configure back.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `display_name` | string | yes |  |
| `fields` | V2CarrierCredentialField[] | yes |  |
| `inbound_style` | string | yes |  |
| `inbound_url_pattern` | string | yes |  |
| `needs` | string | yes | The sentence shown above the connect form. |
| `provider` | string | yes |  |

| Error | When |
| --- | --- |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the machine-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X GET "https://api.developers.dvaarik.com/v2/carriers/catalog" \
  -H "X-Api-Key: $DVAARIK_API_KEY"
```

### GET `/v2/carriers/numbers`

List the project's numbers with the exact answer or stream URL to paste into the carrier.

| Detail | Value |
| --- | --- |
| Door | Project API key — `X-Api-Key: $DVAARIK_API_KEY` |
| Scope | `carrier:read` |
| Operation | `listCarrierNumbers` |
| Success | `200` `V2CarrierNumberInfo[]` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `limit` | query | integer | no |  |
| `offset` | query | integer | no |  |

| Error | When |
| --- | --- |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the machine-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X GET "https://api.developers.dvaarik.com/v2/carriers/numbers?limit=20" \
  -H "X-Api-Key: $DVAARIK_API_KEY"
```

### DELETE `/v2/carriers/numbers/{number_id}`

Forget the number locally. The number itself stays yours at the carrier.

| Detail | Value |
| --- | --- |
| Door | Project API key — `X-Api-Key: $DVAARIK_API_KEY` |
| Scope | `carrier:write` |
| Operation | `deleteCarrierNumber` |
| Success | `204` `no body` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `number_id` | path | uuid | yes |  |

| Error | When |
| --- | --- |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the machine-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X DELETE "https://api.developers.dvaarik.com/v2/carriers/numbers/$NUMBER_ID" \
  -H "X-Api-Key: $DVAARIK_API_KEY"
```

### PATCH `/v2/carriers/numbers/{number_id}`

Bind an agent to the number, and switch inbound answering or outbound dialling on or off.

| Detail | Value |
| --- | --- |
| Door | Project API key — `X-Api-Key: $DVAARIK_API_KEY` |
| Scope | `carrier:write` |
| Operation | `updateCarrierNumber` |
| Success | `200` `V2CarrierNumberInfo` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `number_id` | path | uuid | yes |  |

Request body: `V2CarrierNumberUpdate`.

#### `V2CarrierNumberUpdate`

Every field optional; only what was SENT is applied. `inbound_agent_id=null` explicitly unbinds and switches inbound off, which is why the presence of the key matters and not just its value.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `inbound_agent_id` | uuid or null | no |  |
| `inbound_enabled` | boolean or null | no |  |
| `label` | string or null | no |  |
| `outbound_enabled` | boolean or null | no |  |

| Error | When |
| --- | --- |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the machine-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X PATCH "https://api.developers.dvaarik.com/v2/carriers/numbers/$NUMBER_ID" \
  -H "X-Api-Key: $DVAARIK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "inbound_agent_id": "<agent uuid>",
    "inbound_enabled": true
  }'
```

## Operations — account session

The same resource through the console door. These are what the web console calls; both doors return the same shapes.

### GET `/console/v2/projects/{project_id}/carriers`

List the project's carrier connections and their verification state.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `consoleListCarriers` |
| Success | `200` `V2CarrierInfo[]` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `project_id` | path | uuid | yes |  |
| `limit` | query | integer | no |  |
| `offset` | query | integer | no |  |

| Error | When |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. |
| `403` | The account is suspended or its email is not verified. |
| `404` | The project or the addressed resource does not exist for this owner. A resource owned by another developer is deliberately indistinguishable from one that never existed. |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X GET "https://api.developers.dvaarik.com/console/v2/projects/$PROJECT_ID/carriers?limit=20" \
  -H "Authorization: Bearer $DVAARIK_TOKEN"
```

### POST `/console/v2/projects/{project_id}/carriers`

Connect a carrier account you already pay for. The credentials are checked against the carrier immediately and a rejected credential is never stored.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `consoleCreateCarrier` |
| Success | `201` `V2CarrierInfo` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `project_id` | path | uuid | yes |  |

Request body: `V2CarrierCreate`.

| Error | When |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. |
| `403` | The account is suspended or its email is not verified. |
| `404` | The project or the addressed resource does not exist for this owner. A resource owned by another developer is deliberately indistinguishable from one that never existed. |
| `409` | The project or resource is in a state that refuses this change. |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X POST "https://api.developers.dvaarik.com/console/v2/projects/$PROJECT_ID/carriers" \
  -H "Authorization: Bearer $DVAARIK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "credentials": {
      "<credential field from the catalogue>": "<secret>"
    },
    "name": "Primary line",
    "provider": "<carrier id from the catalogue>"
  }'
```

### DELETE `/console/v2/projects/{project_id}/carriers/{connection_id}`

Forget the carrier credentials and disable every number on the connection. Your carrier account is never touched.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `consoleDisconnectCarrier` |
| Success | `200` `V2CarrierInfo` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `connection_id` | path | uuid | yes |  |
| `project_id` | path | uuid | yes |  |

| Error | When |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. |
| `403` | The account is suspended or its email is not verified. |
| `404` | The project or the addressed resource does not exist for this owner. A resource owned by another developer is deliberately indistinguishable from one that never existed. |
| `409` | The project or resource is in a state that refuses this change. |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X DELETE "https://api.developers.dvaarik.com/console/v2/projects/$PROJECT_ID/carriers/$CONNECTION_ID" \
  -H "Authorization: Bearer $DVAARIK_TOKEN"
```

### GET `/console/v2/projects/{project_id}/carriers/{connection_id}`

Read one carrier connection.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `consoleGetCarrier` |
| Success | `200` `V2CarrierInfo` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `connection_id` | path | uuid | yes |  |
| `project_id` | path | uuid | yes |  |

| Error | When |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. |
| `403` | The account is suspended or its email is not verified. |
| `404` | The project or the addressed resource does not exist for this owner. A resource owned by another developer is deliberately indistinguishable from one that never existed. |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X GET "https://api.developers.dvaarik.com/console/v2/projects/$PROJECT_ID/carriers/$CONNECTION_ID" \
  -H "Authorization: Bearer $DVAARIK_TOKEN"
```

### POST `/console/v2/projects/{project_id}/carriers/{connection_id}/numbers`

Attach one E.164 number by hand, for a carrier that cannot list its numbers.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `consoleAttachCarrierNumber` |
| Success | `201` `V2CarrierNumberInfo` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `connection_id` | path | uuid | yes |  |
| `project_id` | path | uuid | yes |  |

Request body: `V2CarrierNumberAttach`.

| Error | When |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. |
| `403` | The account is suspended or its email is not verified. |
| `404` | The project or the addressed resource does not exist for this owner. A resource owned by another developer is deliberately indistinguishable from one that never existed. |
| `409` | The project or resource is in a state that refuses this change. |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X POST "https://api.developers.dvaarik.com/console/v2/projects/$PROJECT_ID/carriers/$CONNECTION_ID/numbers" \
  -H "Authorization: Bearer $DVAARIK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "phone_number": "+15550000001",
    "label": "Main line"
  }'
```

### POST `/console/v2/projects/{project_id}/carriers/{connection_id}/numbers/import`

Pull the numbers this carrier account already holds.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `consoleImportCarrierNumbers` |
| Success | `200` `V2CarrierNumberImportResult` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `connection_id` | path | uuid | yes |  |
| `project_id` | path | uuid | yes |  |

| Error | When |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. |
| `403` | The account is suspended or its email is not verified. |
| `404` | The project or the addressed resource does not exist for this owner. A resource owned by another developer is deliberately indistinguishable from one that never existed. |
| `409` | The project or resource is in a state that refuses this change. |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X POST "https://api.developers.dvaarik.com/console/v2/projects/$PROJECT_ID/carriers/$CONNECTION_ID/numbers/import" \
  -H "Authorization: Bearer $DVAARIK_TOKEN"
```

### POST `/console/v2/projects/{project_id}/carriers/{connection_id}/verify`

Re-check the stored credentials, or replace them when a body is sent — and only if the carrier accepts the replacement.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `consoleVerifyCarrier` |
| Success | `200` `V2CarrierInfo` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `connection_id` | path | uuid | yes |  |
| `project_id` | path | uuid | yes |  |

| Error | When |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. |
| `403` | The account is suspended or its email is not verified. |
| `404` | The project or the addressed resource does not exist for this owner. A resource owned by another developer is deliberately indistinguishable from one that never existed. |
| `409` | The project or resource is in a state that refuses this change. |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X POST "https://api.developers.dvaarik.com/console/v2/projects/$PROJECT_ID/carriers/$CONNECTION_ID/verify" \
  -H "Authorization: Bearer $DVAARIK_TOKEN"
```

### GET `/console/v2/projects/{project_id}/carriers/catalog`

The connectable carriers with the exact credential fields to render and, per carrier, whether it fetches an answer URL or takes a stream URL.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `consoleListCarrierCatalog` |
| Success | `200` `V2CarrierCatalog` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `project_id` | path | uuid | yes |  |

| Error | When |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. |
| `403` | The account is suspended or its email is not verified. |
| `404` | The project or the addressed resource does not exist for this owner. A resource owned by another developer is deliberately indistinguishable from one that never existed. |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X GET "https://api.developers.dvaarik.com/console/v2/projects/$PROJECT_ID/carriers/catalog" \
  -H "Authorization: Bearer $DVAARIK_TOKEN"
```

### GET `/console/v2/projects/{project_id}/carriers/numbers`

List the project's numbers with the exact answer or stream URL to paste into the carrier.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `consoleListCarrierNumbers` |
| Success | `200` `V2CarrierNumberInfo[]` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `project_id` | path | uuid | yes |  |
| `limit` | query | integer | no |  |
| `offset` | query | integer | no |  |

| Error | When |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. |
| `403` | The account is suspended or its email is not verified. |
| `404` | The project or the addressed resource does not exist for this owner. A resource owned by another developer is deliberately indistinguishable from one that never existed. |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X GET "https://api.developers.dvaarik.com/console/v2/projects/$PROJECT_ID/carriers/numbers?limit=20" \
  -H "Authorization: Bearer $DVAARIK_TOKEN"
```

### DELETE `/console/v2/projects/{project_id}/carriers/numbers/{number_id}`

Forget the number locally. The number itself stays yours at the carrier.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `consoleDeleteCarrierNumber` |
| Success | `204` `no body` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `number_id` | path | uuid | yes |  |
| `project_id` | path | uuid | yes |  |

| Error | When |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. |
| `403` | The account is suspended or its email is not verified. |
| `404` | The project or the addressed resource does not exist for this owner. A resource owned by another developer is deliberately indistinguishable from one that never existed. |
| `409` | The project or resource is in a state that refuses this change. |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X DELETE "https://api.developers.dvaarik.com/console/v2/projects/$PROJECT_ID/carriers/numbers/$NUMBER_ID" \
  -H "Authorization: Bearer $DVAARIK_TOKEN"
```

### PATCH `/console/v2/projects/{project_id}/carriers/numbers/{number_id}`

Bind an agent to the number, and switch inbound answering or outbound dialling on or off.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `consoleUpdateCarrierNumber` |
| Success | `200` `V2CarrierNumberInfo` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `number_id` | path | uuid | yes |  |
| `project_id` | path | uuid | yes |  |

Request body: `V2CarrierNumberUpdate`.

| Error | When |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. |
| `403` | The account is suspended or its email is not verified. |
| `404` | The project or the addressed resource does not exist for this owner. A resource owned by another developer is deliberately indistinguishable from one that never existed. |
| `409` | The project or resource is in a state that refuses this change. |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X PATCH "https://api.developers.dvaarik.com/console/v2/projects/$PROJECT_ID/carriers/numbers/$NUMBER_ID" \
  -H "Authorization: Bearer $DVAARIK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "inbound_agent_id": "<agent uuid>",
    "inbound_enabled": true
  }'
```

# Webhooks API

## What this covers

Signed, retried event delivery per project. The signing secret is returned once at creation; deliveries that exhaust their retries are readable as dead letters.

Every example below runs as written once these are exported:

```bash
export DVAARIK_API_KEY="dvk_..."        # project API key, server side only
export DVAARIK_TOKEN="<account access token>"   # console session, from POST /console/auth/login
export ENDPOINT_ID="<uuid>"
export PROJECT_ID="<uuid>"
```

A `<value>` inside a JSON body is a value you supply. Responses are JSON; every error body is `{ "detail": … }`. Each schema is expanded once per page, where it first appears.

### Machine door errors

Raised by the project-key dependency before any handler on this page runs.

| Status | Cause |
| --- | --- |
| `401` | `Missing API key` or `Invalid API key` — the `X-Api-Key` header is absent, unknown, or revoked. |
| `403` | `API key is not scoped to a project`, `Project unavailable`, or `API key is missing the required scope '<scope>'`. |
| `429` | The project's `api_rpm` ceiling was exceeded. Honour `Retry-After`; `X-RateLimit-*` headers are on every response. |

Scopes used on this page: `webhook:read`, `webhook:write`. Mint a key with only these.

### Account door errors

| Status | Cause |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. Refresh once, then re-authenticate. |
| `403` | The account is suspended or its email is not verified. |

## Operations — project API key

Call these from your own server with a project key. Never from a browser or a mobile bundle.

### GET `/v2/projects/{project_id}/webhooks`

List the project's endpoints. `include_disabled=true` also returns disabled ones.

| Detail | Value |
| --- | --- |
| Door | Project API key — `X-Api-Key: $DVAARIK_API_KEY` |
| Scope | `webhook:read` |
| Operation | `listWebhookEndpoints` |
| Success | `200` `WebhookEndpointInfo[]` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `project_id` | path | uuid | yes |  |
| `include_disabled` | query | boolean | no |  |

#### `WebhookEndpointInfo`

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `created_at` | date-time | yes |  |
| `event_types` | string[] | yes |  |
| `id` | uuid | yes |  |
| `name` | string | yes |  |
| `project_id` | uuid | yes |  |
| `revision` | integer | yes |  |
| `secret_hint` | string | yes |  |
| `status` | string | yes |  |
| `updated_at` | date-time | yes |  |
| `url` | string | yes |  |

| Error | When |
| --- | --- |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the machine-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X GET "https://api.developers.dvaarik.com/v2/projects/$PROJECT_ID/webhooks" \
  -H "X-Api-Key: $DVAARIK_API_KEY"
```

### POST `/v2/projects/{project_id}/webhooks`

Register a public HTTPS endpoint. The `201` carries the signing secret once; no route returns it again.

| Detail | Value |
| --- | --- |
| Door | Project API key — `X-Api-Key: $DVAARIK_API_KEY` |
| Scope | `webhook:write` |
| Operation | `createWebhookEndpoint` |
| Success | `201` `CreatedWebhookEndpoint` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `project_id` | path | uuid | yes |  |

Request body: `CreateWebhookEndpointRequest`.

#### `CreateWebhookEndpointRequest`

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `event_types` | string[] | no | max 50 item(s) |
| `name` | string | yes | min length 1; max length 120 |
| `url` | string | yes | min length 8; max length 2048 |

#### `CreatedWebhookEndpoint`

The 201, and the ONLY response that ever contains the secret.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `created_at` | date-time | yes |  |
| `event_types` | string[] | yes |  |
| `id` | uuid | yes |  |
| `name` | string | yes |  |
| `project_id` | uuid | yes |  |
| `revision` | integer | yes |  |
| `secret_hint` | string | yes |  |
| `signing_secret` | string | yes |  |
| `status` | string | yes |  |
| `updated_at` | date-time | yes |  |
| `url` | string | yes |  |

| Error | When |
| --- | --- |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the machine-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X POST "https://api.developers.dvaarik.com/v2/projects/$PROJECT_ID/webhooks" \
  -H "X-Api-Key: $DVAARIK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "primary",
    "url": "https://example.com/hooks/dvaarik",
    "event_types": [
      "call.completed",
      "call.failed"
    ]
  }'
```

### DELETE `/v2/projects/{project_id}/webhooks/{endpoint_id}`

Disable the endpoint, bump its revision and cancel queued deliveries.

| Detail | Value |
| --- | --- |
| Door | Project API key — `X-Api-Key: $DVAARIK_API_KEY` |
| Scope | `webhook:write` |
| Operation | `disableWebhookEndpoint` |
| Success | `200` `WebhookEndpointInfo` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `project_id` | path | uuid | yes |  |
| `endpoint_id` | path | uuid | yes |  |

| Error | When |
| --- | --- |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the machine-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X DELETE "https://api.developers.dvaarik.com/v2/projects/$PROJECT_ID/webhooks/$ENDPOINT_ID" \
  -H "X-Api-Key: $DVAARIK_API_KEY"
```

### POST `/v2/projects/{project_id}/webhooks/{endpoint_id}/test`

Queue a signed `webhook.ping`. The `202` means the delivery was recorded, not that it arrived.

| Detail | Value |
| --- | --- |
| Door | Project API key — `X-Api-Key: $DVAARIK_API_KEY` |
| Scope | `webhook:write` |
| Operation | `testWebhookEndpoint` |
| Success | `202` `WebhookTestResult` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `project_id` | path | uuid | yes |  |
| `endpoint_id` | path | uuid | yes |  |

#### `WebhookTestResult`

What a test ping queued, not what a receiver said. `queued` is False when an identical ping is already waiting: the dedupe key is the endpoint plus its revision plus the minute, so leaning on the button does not turn into a burst against the developer's own server. The delivery id is returned either way so the caller can find it in the dead letters.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `delivery_id` | uuid | yes |  |
| `endpoint_id` | uuid | yes |  |
| `event_type` | string | yes |  |
| `queued` | boolean | yes |  |

| Error | When |
| --- | --- |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the machine-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X POST "https://api.developers.dvaarik.com/v2/projects/$PROJECT_ID/webhooks/$ENDPOINT_ID/test" \
  -H "X-Api-Key: $DVAARIK_API_KEY"
```

### GET `/v2/projects/{project_id}/webhooks/dead-letters`

List deliveries that exhausted their retries, with attempt counts and the last error.

| Detail | Value |
| --- | --- |
| Door | Project API key — `X-Api-Key: $DVAARIK_API_KEY` |
| Scope | `webhook:read` |
| Operation | `listWebhookDeadLetters` |
| Success | `200` `DeadLetterInfo[]` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `project_id` | path | uuid | yes |  |
| `limit` | query | integer | no |  |

#### `DeadLetterInfo`

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `attempts` | integer | yes |  |
| `created_at` | date-time | yes |  |
| `endpoint_id` | uuid or null | no |  |
| `event_type` | string or null | no |  |
| `id` | uuid | yes |  |
| `kind` | string | yes |  |
| `last_error` | string or null | no |  |
| `max_attempts` | integer | yes |  |
| `updated_at` | date-time | yes |  |

| Error | When |
| --- | --- |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the machine-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X GET "https://api.developers.dvaarik.com/v2/projects/$PROJECT_ID/webhooks/dead-letters?limit=20" \
  -H "X-Api-Key: $DVAARIK_API_KEY"
```

## Operations — account session

The same resource through the console door. These are what the web console calls; both doors return the same shapes.

### GET `/console/v2/projects/{project_id}/webhooks`

List the project's endpoints. `include_disabled=true` also returns disabled ones.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `consoleListWebhookEndpoints` |
| Success | `200` `WebhookEndpointInfo[]` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `project_id` | path | uuid | yes |  |
| `include_disabled` | query | boolean | no |  |

| Error | When |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. |
| `403` | The account is suspended or its email is not verified. |
| `404` | The project or the addressed resource does not exist for this owner. A resource owned by another developer is deliberately indistinguishable from one that never existed. |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X GET "https://api.developers.dvaarik.com/console/v2/projects/$PROJECT_ID/webhooks" \
  -H "Authorization: Bearer $DVAARIK_TOKEN"
```

### POST `/console/v2/projects/{project_id}/webhooks`

Register a public HTTPS endpoint. The `201` carries the signing secret once; no route returns it again.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `consoleCreateWebhookEndpoint` |
| Success | `201` `CreatedWebhookEndpoint` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `project_id` | path | uuid | yes |  |

Request body: `CreateWebhookEndpointRequest`.

| Error | When |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. |
| `403` | The account is suspended or its email is not verified. |
| `404` | The project or the addressed resource does not exist for this owner. A resource owned by another developer is deliberately indistinguishable from one that never existed. |
| `409` | The project or resource is in a state that refuses this change. |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |
| `503` | Webhook secret storage (the credential keyring) is unavailable. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X POST "https://api.developers.dvaarik.com/console/v2/projects/$PROJECT_ID/webhooks" \
  -H "Authorization: Bearer $DVAARIK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "primary",
    "url": "https://example.com/hooks/dvaarik",
    "event_types": [
      "call.completed",
      "call.failed"
    ]
  }'
```

### DELETE `/console/v2/projects/{project_id}/webhooks/{endpoint_id}`

Disable the endpoint, bump its revision and cancel queued deliveries.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `consoleDisableWebhookEndpoint` |
| Success | `200` `WebhookEndpointInfo` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `endpoint_id` | path | uuid | yes |  |
| `project_id` | path | uuid | yes |  |

| Error | When |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. |
| `403` | The account is suspended or its email is not verified. |
| `404` | The project or the addressed resource does not exist for this owner. A resource owned by another developer is deliberately indistinguishable from one that never existed. |
| `409` | The project or resource is in a state that refuses this change. |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X DELETE "https://api.developers.dvaarik.com/console/v2/projects/$PROJECT_ID/webhooks/$ENDPOINT_ID" \
  -H "Authorization: Bearer $DVAARIK_TOKEN"
```

### POST `/console/v2/projects/{project_id}/webhooks/{endpoint_id}/test`

Queue a signed `webhook.ping`. The `202` means the delivery was recorded, not that it arrived.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `consoleTestWebhookEndpoint` |
| Success | `202` `WebhookTestResult` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `endpoint_id` | path | uuid | yes |  |
| `project_id` | path | uuid | yes |  |

| Error | When |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. |
| `403` | The account is suspended or its email is not verified. |
| `404` | The project or the addressed resource does not exist for this owner. A resource owned by another developer is deliberately indistinguishable from one that never existed. |
| `409` | The project or resource is in a state that refuses this change. |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X POST "https://api.developers.dvaarik.com/console/v2/projects/$PROJECT_ID/webhooks/$ENDPOINT_ID/test" \
  -H "Authorization: Bearer $DVAARIK_TOKEN"
```

### GET `/console/v2/projects/{project_id}/webhooks/dead-letters`

List deliveries that exhausted their retries, with attempt counts and the last error.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `consoleListWebhookDeadLetters` |
| Success | `200` `DeadLetterInfo[]` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `project_id` | path | uuid | yes |  |
| `limit` | query | integer | no |  |

| Error | When |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. |
| `403` | The account is suspended or its email is not verified. |
| `404` | The project or the addressed resource does not exist for this owner. A resource owned by another developer is deliberately indistinguishable from one that never existed. |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X GET "https://api.developers.dvaarik.com/console/v2/projects/$PROJECT_ID/webhooks/dead-letters?limit=20" \
  -H "Authorization: Bearer $DVAARIK_TOKEN"
```

# Billing API

## What this covers

A prepaid USD wallet per account. Read the balance and the rate, list transactions and invoices, and open a checkout to top up. Console door only — a project key does not move money.

Every example below runs as written once these are exported:

```bash
export DVAARIK_TOKEN="<account access token>"   # console session, from POST /console/auth/login
export INVOICE_ID="<uuid>"
```

A `<value>` inside a JSON body is a value you supply. Responses are JSON; every error body is `{ "detail": … }`. Each schema is expanded once per page, where it first appears.

### Account door errors

| Status | Cause |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. Refresh once, then re-authenticate. |
| `403` | The account is suspended or its email is not verified. |

## Operations — account session

The same resource through the console door. These are what the web console calls; both doors return the same shapes.

### GET `/console/v2/billing/invoices`

Receipts for paid top-ups.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `consoleListBillingInvoices` |
| Success | `200` `V2InvoiceInfo[]` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `limit` | query | integer | no |  |
| `offset` | query | integer | no |  |

#### `V2InvoiceInfo`

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `amount_minor` | integer or null | no |  |
| `credits_nano_usd` | string or null | no |  |
| `currency` | string | yes |  |
| `id` | uuid | yes |  |
| `issued_at` | date-time | yes |  |
| `number` | string | yes |  |
| `project_id` | uuid or null | yes |  |

| Error | When |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. |
| `403` | The account is suspended or its email is not verified. |
| `404` | The project or the addressed resource does not exist for this owner. A resource owned by another developer is deliberately indistinguishable from one that never existed. |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X GET "https://api.developers.dvaarik.com/console/v2/billing/invoices?limit=20" \
  -H "Authorization: Bearer $DVAARIK_TOKEN"
```

### GET `/console/v2/billing/invoices/{invoice_id}.pdf`

Download one receipt as a PDF.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `consoleDownloadBillingInvoicePdf` |
| Success | `200` `application/pdf` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `invoice_id` | path | uuid | yes |  |

| Error | When |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. |
| `403` | The account is suspended or its email is not verified. |
| `404` | The project or the addressed resource does not exist for this owner. A resource owned by another developer is deliberately indistinguishable from one that never existed. |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X GET "https://api.developers.dvaarik.com/console/v2/billing/invoices/$INVOICE_ID.pdf" \
  -H "Authorization: Bearer $DVAARIK_TOKEN"
```

### GET `/console/v2/billing/quote`

What a top-up buys, before you start it. No fee and no markup: one cent is exactly 10,000,000 nano-USD.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `consoleGetTopupQuote` |
| Success | `200` `V2TopupQuote` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `amount_usd_cents` | query | integer | yes |  |

#### `V2TopupQuote`

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `amount_usd_cents` | integer | yes |  |
| `credits_nano_usd` | string | yes |  |
| `currency` | string | no | default "USD" |
| `money_scale` | integer | yes |  |

| Error | When |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. |
| `403` | The account is suspended or its email is not verified. |
| `404` | The project or the addressed resource does not exist for this owner. A resource owned by another developer is deliberately indistinguishable from one that never existed. |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X GET "https://api.developers.dvaarik.com/console/v2/billing/quote?amount_usd_cents=<amount_usd_cents>" \
  -H "Authorization: Bearer $DVAARIK_TOKEN"
```

### GET `/console/v2/billing/rate`

The connected-minute rate on its own, with its metering basis and rate version.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `consoleGetVoiceRate` |
| Success | `200` `VoiceRateResponse` |

#### `VoiceRateResponse`

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `voice_rate` | VoiceRateContract | yes | The one price this product charges. |

#### `VoiceRateContract`

What one connected minute costs, in both forms the console needs.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `currency` | string | no | default "USD" |
| `metering_basis` | string | no | default "connected_minute_rounded_up" |
| `money_scale` | integer | no | default 1000000000 |
| `rate_display` | string | yes |  |
| `rate_nano_usd_per_minute` | string | yes |  |
| `rate_version` | string | yes |  |

| Error | When |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. |
| `403` | The account is suspended or its email is not verified. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X GET "https://api.developers.dvaarik.com/console/v2/billing/rate" \
  -H "Authorization: Bearer $DVAARIK_TOKEN"
```

### GET `/console/v2/billing/summary`

The account wallet — balance, held, spendable, lifetime top-up, low-balance flag — with the rate and this project's limits.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `consoleGetBillingSummary` |
| Success | `200` `V2BillingSummary` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `project_id` | query | uuid or null | no |  |

#### `V2BillingSummary`

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `api_requests_per_minute` | integer | yes |  |
| `balance_nano_usd` | string | yes |  |
| `currency` | string | no | default "USD" |
| `held_nano_usd` | string | yes |  |
| `lifetime_topup_nano_usd` | string | yes |  |
| `low_balance` | boolean | yes |  |
| `low_balance_threshold_nano_usd` | string | yes |  |
| `max_voice_session_seconds` | integer | yes |  |
| `money_scale` | integer | no | default 1000000000 |
| `project_id` | uuid | yes |  |
| `spendable_nano_usd` | string | yes |  |
| `usd_state` | string | yes |  |
| `voice_concurrency` | integer | yes |  |
| `voice_rate` | VoiceRateContract | yes |  |

| Error | When |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. |
| `403` | The account is suspended or its email is not verified. |
| `404` | The project or the addressed resource does not exist for this owner. A resource owned by another developer is deliberately indistinguishable from one that never existed. |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X GET "https://api.developers.dvaarik.com/console/v2/billing/summary" \
  -H "Authorization: Bearer $DVAARIK_TOKEN"
```

### POST `/console/v2/billing/topup`

Record a top-up order and open a Razorpay USD checkout. The wallet is credited by the payment webhook, never by a successful checkout alone.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `consoleStartTopup` |
| Success | `200` `V2TopupOrderResponse` |

Request body: `V2TopupRequest`.

#### `V2TopupRequest`

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `amount_usd_cents` | integer | yes |  |
| `project_id` | uuid or null | no |  |

#### `V2TopupOrderResponse`

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `amount_usd_cents` | integer | yes |  |
| `credits_nano_usd` | string | yes |  |
| `currency` | string | no | default "USD" |
| `money_scale` | integer | yes |  |
| `order_id` | string | yes |  |
| `project_id` | uuid | yes |  |
| `razorpay_key_id` | string | yes |  |

| Error | When |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. |
| `403` | The account is suspended or its email is not verified. |
| `404` | The project or the addressed resource does not exist for this owner. A resource owned by another developer is deliberately indistinguishable from one that never existed. |
| `409` | The project or resource is in a state that refuses this change. |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |
| `502` | The payment gateway did not create an order. |
| `503` | The order could not be recorded locally, so no checkout was returned. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X POST "https://api.developers.dvaarik.com/console/v2/billing/topup" \
  -H "Authorization: Bearer $DVAARIK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "amount_usd_cents": 2500
  }'
```

### GET `/console/v2/billing/transactions`

The ledger, newest first.

| Detail | Value |
| --- | --- |
| Door | Account session — `Authorization: Bearer $DVAARIK_TOKEN` |
| Operation | `consoleListBillingTransactions` |
| Success | `200` `V2TransactionInfo[]` |

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `limit` | query | integer | no |  |
| `offset` | query | integer | no |  |

#### `V2TransactionInfo`

One canonical USD ledger row. A row from before the USD cutover has no USD amount and is deliberately NOT rendered as a zero: `amount_nano_usd` is null and the caller can tell "nothing" from "nothing yet converted". The archival paise figures stay on the legacy route.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `amount_nano_usd` | string or null | no |  |
| `balance_after_nano_usd` | string or null | no |  |
| `created_at` | date-time | yes |  |
| `currency` | string | yes |  |
| `description` | string or null | no |  |
| `id` | uuid | yes |  |
| `money_scale` | integer | yes |  |
| `project_id` | uuid or null | yes |  |
| `reference` | string or null | no |  |
| `type` | string | yes |  |

| Error | When |
| --- | --- |
| `401` | No account session was presented, or it is invalid or expired. |
| `403` | The account is suspended or its email is not verified. |
| `404` | The project or the addressed resource does not exist for this owner. A resource owned by another developer is deliberately indistinguishable from one that never existed. |
| `422` | The request body or query is not valid. `detail` is an array of `{loc, msg, type}` and `loc` names the offending field. |

Plus the account-door errors above. Full list on the [errors page](/docs/errors).

```bash
curl -sS -X GET "https://api.developers.dvaarik.com/console/v2/billing/transactions?limit=20" \
  -H "Authorization: Bearer $DVAARIK_TOKEN"
```
