Developer platform · v2

Platform

View as MarkdownAll docs

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.

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.

Or an outbound call on a number you already own, once a carrier is connected under 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 — 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. 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 and see 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 — the two doors, and what a 401 means.
  • API reference — one page per resource, every operation with a curl example.
  • Errors — every status this API returns and what to do about it.

Use one project per boundary

Provider secrets stay encrypted and project API keys stay on your server.