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

---

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