Developer API (/v1)
The API-key surface, versioned separately from the console API so the console can change shape without breaking an integration.
Authorization: Bearer nn_k_your_key_here
Nine endpoints across calls, contacts, and campaigns. Anything not listed here is console-only and needs a session — see the pages for calls, customers, and flows.
Response envelope
Every /v1 response wraps its payload in data:
{ "data": { "...": "..." } }
Lists add total:
{ "data": [ "..." ], "total": 891 }
The envelope exists so pagination cursors can be added later without breaking existing integrations. Console endpoints use a different, unwrapped shape — do not reuse a parser across the two.
Calls
List calls
GET /v1/calls · scope calls:read
Same filters as the console list: call_type, outcome, sentiment,
flow_uuid, date_from, date_to, search, limit, offset.
curl "https://your-host/v1/calls?call_type=outbound&limit=2" \
-H "Authorization: Bearer nn_k_your_key_here"
{
"data": [
{
"uuid": "cal-c1d2e3f4-…",
"call_type": "outbound",
"from_number": "+915550010000",
"to_number": "+918888888888",
"duration": 113,
"duration_formatted": "1:53",
"outcome": "initiated",
"sentiment": null,
"summary": null,
"recording_url": "https://…/recording.mp3",
"started_at": "2026-09-20T14:32:01Z",
"ended_at": "2026-09-20T14:33:54Z",
"customer_name": "Priya Kumar",
"flow_name": "September winbacks",
"agent_name": "Aria"
}
],
"total": 891
}
See outcomes before filtering on outcome —
outbound calls stay initiated, and sentiment and summary are never
populated.
Get a call
GET /v1/calls/{uuid} · scope calls:read
Includes the full transcript.
Call statistics
GET /v1/calls/statistics · scope calls:read
Takes flow_uuid, date_from, date_to.
{
"data": {
"total_calls": 891,
"connected_calls": 543,
"calls_today": 42,
"avg_duration": 97,
"connect_rate": 0.61
}
}
Contacts
List contacts
GET /v1/contacts · scope customers:read
Takes search, tags, limit, offset.
Create or update a contact
POST /v1/contacts · scope customers:write
{
"name": "Priya Kumar",
"phone": "+918888888888",
"email": "[email protected]",
"tags": ["winback"],
"preferred_language": "hi-IN",
"metadata": { "cart_value": 850 }
}
Both name and phone are required here — unlike the console endpoint, an
email-only contact is rejected.
This upserts on phone number. An integration syncing a CRM resends the same
contact constantly, and a hard conflict would make every re-sync fail. Expect
201 whether the contact was created or updated.
The upsert matches on the exact phone string. +918888888888 and
8888888888 are different contacts — normalise to E.164 before syncing.
Update a contact
PATCH /v1/contacts/{uuid} · scope customers:write
Fields are applied with COALESCE, so omitted fields keep their stored value
and email cannot be cleared once set.
Delete a contact
DELETE /v1/contacts/{uuid} · scope customers:write → 204
Campaigns
Read-only on this surface. Starting and stopping a campaign is a console action — see Campaigns.
List campaigns
GET /v1/campaigns · scope flows:read
Get a campaign
GET /v1/campaigns/{uuid} · scope flows:read
{
"data": {
"uuid": "f1g2h3i4-…",
"kind": "outbound",
"name": "September winbacks",
"status": "active",
"customer_count": 240,
"branches": ["winback", "north"],
"config": { "calls_per_minute": 10 },
"voice_agent": { "uuid": "a1b2…", "name": "Aria" }
}
}
Scopes
| Endpoint | Scope |
|---|---|
GET /v1/calls, /v1/calls/{uuid}, /v1/calls/statistics | calls:read |
GET /v1/contacts | customers:read |
POST, PATCH, DELETE /v1/contacts | customers:write |
GET /v1/campaigns, /v1/campaigns/{uuid} | flows:read |
A key missing the scope gets 403. Issue keys in
API keys.
Tag scoping applies
The list endpoints honour the tag restrictions of the admin who created the
key. A key created by a teammate restricted to north lists only north
customers and their calls. Single-resource lookups by UUID are not
re-checked — see the note in Customers.
A typical integration
1. POST /v1/contacts ← push today's leads from your CRM
2. (console) start the campaign
3. webhook: call.ended ← receive the transcript as each call finishes
4. GET /v1/calls/{uuid} ← fetch anything the webhook did not carry
5. write back to your CRM
Webhooks are the push half of this. Registering one needs a console session; delivery then needs nothing from you.