Developer API

Build on NestVoice

Place AI calls, read transcripts and receive results from your own software with a simple REST API.

Built for builders
Illustration of a code editor window with a code tag
01Guides02API03Webhooks
Direct answer

How do I place an AI call through the NestVoice API?

Create an API key in your dashboard under Developers, then send a POST request to the calls endpoint with an agent id and a phone number, using your key as a Bearer token. The call goes out from your own connected number and is billed like any dashboard call; results arrive by signed webhook.

Base URL
https://api.nestvoiceai.com/v1
Auth
Bearer API key with scopes
Webhooks
call.completed, call.failed, lead.qualified, meeting.created
Plans
Included in every paid plan

Overview

With the NestVoice API your own server can place an AI phone call, check how it went, and read the transcript, summary and recording. You can also list and create agents.

  • An agent is a campaign in your dashboard: the prompt, greeting, voice, language, knowledge base and follow-ups you already set up. Set it up once in the dashboard, then call it from code.
  • Calls are billed exactly like calls placed from the dashboard: the same per-minute prices and the same credits.
  • Calls go out from your own connected numbers (the Exotel, Twilio or Plivo account you connected).
  • The API is included in every paid plan.

Authentication

Create a key in Dashboard, Developers, API keys. Send it as a Bearer token on every request. Keys start with nv_live_ and are shown only once when created.

Authorization: Bearer nv_live_<your_secret>
  • Keep keys on your server, in environment variables. Never put one in a browser or mobile app; the API does not allow browser (CORS) requests.
  • Each key has scopes: agents:read, agents:write, calls:read, calls:write. A missing scope returns 403 missing_scope.
  • To rotate a key, create a new one, deploy it, then revoke the old one in the dashboard. Revoked keys stop working immediately.
  • Every response carries an X-Request-Id header. Quote it when you contact support.

Base URL

https://api.nestvoiceai.com/v1

All requests use HTTPS and JSON, except the recording download which returns audio.

Errors

Every error has the same shape. Branch on type and code; show message to a human.

{
  "error": {
    "type": "permission",
    "code": "missing_scope",
    "message": "This key needs the calls:write scope.",
    "request_id": "req_8d3f0c"
  }
}
typeHTTPMeaning
invalid_request400, 422The request is malformed or a field is invalid.
authentication401Missing, wrong or revoked key.
insufficient_credits402Not enough credits to place the call.
permission403The key lacks a scope, the plan is not paid, or the number is on your do-not-call list.
not_found404That agent, call or recording does not exist in your workspace.
conflict409The call cannot be placed or changed right now (hours, lines busy, already ended, idempotency).
rate_limited429Too many requests; wait for Retry-After seconds.
server_error500Something went wrong on our side. Safe to retry with an Idempotency-Key.

Common codes

invalid_api_keyThe key is unknown. Check for typos or extra spaces.
key_revokedThe key was revoked in the dashboard.
paid_plan_requiredThe API needs a paid plan.
missing_scopeThe key does not have the scope this endpoint needs.
validation_errorA field failed validation; the message says which.
agent_not_found / call_not_foundNo such agent or call in this workspace.
agent_cannot_dialThe agent is inbound-only and cannot place calls.
invalid_phone_numberto is not a dialable Indian number.
number_not_yoursfrom is not one of your connected numbers.
do_not_callto is on your workspace do-not-call list.
outside_calling_hoursThe agent's calling hours do not allow a call now.
insufficient_credits / plan_expiredTop up credits or renew the plan.
lines_busy and its variantsworkspace_lines_full, no_number, numbers_busy, number_outside_hours, contact_busy, provider_busy, platform_busy: no line could take the call. Retry later.
call_already_endedThe call is already over.
recording_not_availableThis call has no recording.
idempotency_key_reused / idempotency_key_in_useThe same Idempotency-Key was used with a different body, or its first request is still running.
rate_limitedSlow down and retry after Retry-After.

Rate limits

Each key may make 120 requests per minute, and at most 30 of those may be POST /v1/calls. Every response includes X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (unix seconds). When you exceed a limit you get 429 rate_limited with a Retry-After header: wait that many seconds.

Pagination

List endpoints return newest first. Pass limit (1 to 100, default 20) and cursor. While has_more is true, request the next page with cursor set to next_cursor.

{ "object": "list", "data": [ ... ], "has_more": true, "next_cursor": "eyJvIjoyMH0" }

Idempotency

Network errors happen. Send an Idempotency-Key header (8 to 255 characters, for example your order id) on POST /v1/calls and POST /v1/agents so a retry can never place a second call.

  • Same key and same body within 24 hours: you get the original response again, with the header Idempotent-Replayed: true.
  • Same key with a different body: 409 idempotency_key_reused.
  • Same key while the first request is still running: 409 idempotency_key_in_use.

Account

GET/v1/meScope: any scope

Check your key, plan, credits and lines. A good first request to test a new key.

Response

{
  "object": "workspace",
  "id": "ws_42",
  "name": "Acme Retail",
  "key": { "id": "key_1", "name": "Production server", "prefix": "nv_live_a1b2", "scopes": ["agents:read", "calls:write"] },
  "credits": { "balance_inr": 1840.5, "balance_credits": 3681 },
  "plan": { "name": "Growth", "valid_until": "2026-12-01T00:00:00Z" },
  "lines": 4
}

Agents

An agent is a campaign in your dashboard: its prompt, greeting, voice, language, knowledge base and follow-ups. Whatever you have already configured there is what the API uses. Agents cannot be deleted through the API.

GET/v1/agentsScope: agents:read

List your agents, newest first.

Query parameters

typestringFilter by outbound, inbound or bidirectional.
limitinteger1 to 100, default 20.
cursorstringnext_cursor from the previous page.
curl "https://api.nestvoiceai.com/v1/agents?type=outbound&limit=20" \
  -H "Authorization: Bearer $NESTVOICE_API_KEY"

Response

{
  "object": "list",
  "data": [
    {
      "id": "agt_123",
      "object": "agent",
      "name": "Order confirmation",
      "type": "outbound",
      "status": "active",
      "language": "en-IN",
      "voice_tier": "sarvam",
      "voice": "anushka",
      "greeting": "Hi {{customer_name}}, this is Priya from {{business_name}}.",
      "prompt": "Confirm the customer's order and delivery address...",
      "goal": "Confirm the order",
      "variables": ["customer_name"],
      "knowledge_base_ids": [],
      "collected_fields": [{ "key": "address_ok", "label": "Address confirmed", "type": "boolean" }],
      "created_at": "2026-11-01T09:30:00Z",
      "updated_at": "2026-11-03T12:00:00Z"
    }
  ],
  "has_more": true,
  "next_cursor": "eyJvIjoyMH0"
}
GET/v1/agents/{agent_id}Scope: agents:read

Fetch one agent. The variables array lists the {{placeholders}} you can fill when placing a call.

Response

{
  "id": "agt_123",
  "object": "agent",
  "name": "Order confirmation",
  "type": "outbound",
  "status": "active",
  "language": "en-IN",
  "voice_tier": "sarvam",
  "voice": "anushka",
  "greeting": "Hi {{customer_name}}, this is Priya from {{business_name}}.",
  "prompt": "Confirm the customer's order and delivery address...",
  "goal": "Confirm the order",
  "variables": ["customer_name"],
  "knowledge_base_ids": [],
  "collected_fields": [{ "key": "address_ok", "label": "Address confirmed", "type": "boolean" }],
  "created_at": "2026-11-01T09:30:00Z",
  "updated_at": "2026-11-03T12:00:00Z"
}
POST/v1/agentsScope: agents:write

Create an agent. Same validation and plan limits as creating a campaign in the dashboard.

Request body

name*stringDisplay name.
typestringoutbound (default), inbound or bidirectional.
promptstringInstructions for the AI. May contain {{placeholders}}.
greetingstringFirst sentence spoken. May contain {{placeholders}}.
goalstringWhat a successful call achieves.
languagestringLanguage code, for example en-IN or hi-IN.
voice_tierstringVoice quality tier enabled for your workspace.
voicestringVoice id from GET /v1/voices.
knowledge_base_idsstring[]Knowledge bases the agent may use.
collected_fieldsobject[]Fields to extract: {key, label, type}.
  • Supports the Idempotency-Key header.

Request body

{
  "name": "Order confirmation",
  "type": "outbound",
  "greeting": "Hi {{customer_name}}, this is Priya from {{business_name}}.",
  "prompt": "Confirm the customer's order and delivery address.",
  "language": "en-IN"
}

Response 201 Created

{
  "id": "agt_123",
  "object": "agent",
  "name": "Order confirmation",
  "type": "outbound",
  "status": "active",
  "language": "en-IN",
  "voice_tier": "sarvam",
  "voice": "anushka",
  "greeting": "Hi {{customer_name}}, this is Priya from {{business_name}}.",
  "prompt": "Confirm the customer's order and delivery address...",
  "goal": "Confirm the order",
  "variables": ["customer_name"],
  "knowledge_base_ids": [],
  "collected_fields": [{ "key": "address_ok", "label": "Address confirmed", "type": "boolean" }],
  "created_at": "2026-11-01T09:30:00Z",
  "updated_at": "2026-11-03T12:00:00Z"
}
PATCH/v1/agents/{agent_id}Scope: agents:write

Change any of the fields above. Only the fields you send are updated.

Request body

{ "greeting": "Hello {{customer_name}}!" }

Response

{
  "id": "agt_123",
  "object": "agent",
  "name": "Order confirmation",
  "type": "outbound",
  "status": "active",
  "language": "en-IN",
  "voice_tier": "sarvam",
  "voice": "anushka",
  "greeting": "Hi {{customer_name}}, this is Priya from {{business_name}}.",
  "prompt": "Confirm the customer's order and delivery address...",
  "goal": "Confirm the order",
  "variables": ["customer_name"],
  "knowledge_base_ids": [],
  "collected_fields": [{ "key": "address_ok", "label": "Address confirmed", "type": "boolean" }],
  "created_at": "2026-11-01T09:30:00Z",
  "updated_at": "2026-11-03T12:00:00Z"
}

Voices and phone numbers

GET/v1/voicesScope: agents:read

Voices your workspace can use. Only tiers that are enabled, included in your plan and configured are listed.

Query parameters

tierstringFilter by voice tier.
languagestringFilter by language.

Response

{
  "object": "list",
  "data": [
    { "id": "anushka", "name": "Anushka", "tier": "sarvam", "gender": "female", "language": "en-IN", "accent": "Indian", "description": "Warm and clear" }
  ],
  "has_more": false,
  "next_cursor": null
}
GET/v1/phone-numbersScope: calls:read

Your own connected numbers. Any of these may be used as from when placing a call.

Response

{
  "object": "list",
  "data": [
    { "id": "num_1", "object": "phone_number", "number": "+918012345678", "provider": "exotel", "label": "Sales line", "agent_id": "agt_123" }
  ],
  "has_more": false,
  "next_cursor": null
}

Calls

Calls go out from your own connected numbers and are billed exactly like calls placed from the dashboard (same per-minute prices, same credits). The list also includes calls from the dashboard, campaigns and inbound calls; the source field tells them apart.

POST/v1/callsScope: calls:write

Place an outbound call. Returns as soon as the call is dialling; use webhooks or GET /v1/calls/{id} for the result.

Request body

agent_id*stringAn outbound or bidirectional agent of your workspace.
to*stringIndian number: +919876543210, 919876543210, 09876543210 or 9876543210. Normalised for you.
fromstringOne of your own numbers. Default: the number the dashboard would pick.
variablesobjectstring to string, up to 50 keys (letters, digits, underscore; max 41 chars), values up to 500 chars. Fills {{key}} in the greeting and prompt for this call only.
metadataobjectStrings, numbers or booleans, up to 50 keys and 8 KB. Stored, returned on the call and in webhooks. Never shown to the AI.
  • Checks run in this order: credits and plan (402), your do-not-call list (403 do_not_call), compliance, the agent's calling hours (409 outside_calling_hours), line and number availability (for example 409 workspace_lines_full or numbers_busy), then dialling.
  • Supports the Idempotency-Key header, so a retried request never places a second call.
curl -X POST https://api.nestvoiceai.com/v1/calls \
  -H "Authorization: Bearer $NESTVOICE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042-call" \
  -d '{
    "agent_id": "agt_123",
    "to": "+919876543210",
    "variables": { "customer_name": "Asha" },
    "metadata": { "order_id": "1042" }
  }'

Request body

{
  "agent_id": "agt_123",
  "to": "+919876543210",
  "variables": { "customer_name": "Asha" },
  "metadata": { "order_id": "1042" }
}

Response 201 Created

{
  "id": "call_abc123",
  "object": "call",
  "agent_id": "agt_123",
  "direction": "outbound",
  "status": "ringing",
  "to": "+919876543210",
  "from": "+918012345678",
  "variables": { "customer_name": "Asha" },
  "metadata": { "order_id": "1042" },
  "created_at": "2026-11-04T10:00:00Z",
  "started_at": null,
  "answered_at": null,
  "ended_at": null,
  "duration_seconds": null,
  "source": "api"
}
GET/v1/callsScope: calls:read

List calls, newest first.

Query parameters

agent_idstringOnly calls of this agent.
statusstringOne of the call statuses below.
directionstringoutbound or inbound.
created_afterISO 8601Only calls created after this time.
created_beforeISO 8601Only calls created before this time.
limitinteger1 to 100, default 20.
cursorstringnext_cursor from the previous page.

Response

{
  "object": "list",
  "data": [
    {
      "id": "call_abc123",
      "object": "call",
      "agent_id": "agt_123",
      "direction": "outbound",
      "status": "completed",
      "to": "+919876543210",
      "from": "+918012345678",
      "variables": { "customer_name": "Asha" },
      "metadata": { "order_id": "1042" },
      "created_at": "2026-11-04T10:00:00Z",
      "started_at": "2026-11-04T10:00:02Z",
      "answered_at": "2026-11-04T10:00:09Z",
      "ended_at": "2026-11-04T10:01:41Z",
      "duration_seconds": 92,
      "billed_minutes": 2,
      "cost_inr": 12.0,
      "cost_credits": 24,
      "voice_tier": "sarvam",
      "language": "en-IN",
      "outcome": "confirmed",
      "sentiment": "positive",
      "summary": "Customer confirmed the order and delivery address.",
      "collected_data": { "address_ok": true },
      "recording_available": true,
      "failure_reason": null,
      "source": "api"
    }
  ],
  "has_more": true,
  "next_cursor": "eyJvIjoyMH0"
}
GET/v1/calls/{call_id}Scope: calls:read

Fetch one call with its outcome, summary, extracted data and cost.

curl https://api.nestvoiceai.com/v1/calls/call_abc123 \
  -H "Authorization: Bearer $NESTVOICE_API_KEY"

Response

{
  "id": "call_abc123",
  "object": "call",
  "agent_id": "agt_123",
  "direction": "outbound",
  "status": "completed",
  "to": "+919876543210",
  "from": "+918012345678",
  "variables": { "customer_name": "Asha" },
  "metadata": { "order_id": "1042" },
  "created_at": "2026-11-04T10:00:00Z",
  "started_at": "2026-11-04T10:00:02Z",
  "answered_at": "2026-11-04T10:00:09Z",
  "ended_at": "2026-11-04T10:01:41Z",
  "duration_seconds": 92,
  "billed_minutes": 2,
  "cost_inr": 12.0,
  "cost_credits": 24,
  "voice_tier": "sarvam",
  "language": "en-IN",
  "outcome": "confirmed",
  "sentiment": "positive",
  "summary": "Customer confirmed the order and delivery address.",
  "collected_data": { "address_ok": true },
  "recording_available": true,
  "failure_reason": null,
  "source": "api"
}
GET/v1/calls/{call_id}/transcriptScope: calls:read

The conversation, turn by turn.

  • role is agent, caller or human (a person who took over).

Response

{
  "object": "transcript",
  "call_id": "call_abc123",
  "data": [
    { "role": "agent", "text": "Hi Asha, this is Priya from Acme Retail.", "timestamp": "2026-11-04T10:00:10Z" },
    { "role": "caller", "text": "Hello, yes.", "timestamp": "2026-11-04T10:00:14Z" }
  ]
}
GET/v1/calls/{call_id}/recordingScope: calls:read

The call audio as audio/mpeg bytes (not JSON). Returns 404 recording_not_available when there is none.

Response

HTTP/1.1 200 OK
Content-Type: audio/mpeg

<binary audio>
POST/v1/calls/{call_id}/endScope: calls:write

Hang up a call that is still in progress. Returns 409 call_already_ended if it is over.

Response

{
  "id": "call_abc123",
  "object": "call",
  "agent_id": "agt_123",
  "direction": "outbound",
  "status": "completed",
  "to": "+919876543210",
  "from": "+918012345678",
  "variables": { "customer_name": "Asha" },
  "metadata": { "order_id": "1042" },
  "created_at": "2026-11-04T10:00:00Z",
  "started_at": "2026-11-04T10:00:02Z",
  "answered_at": "2026-11-04T10:00:09Z",
  "ended_at": "2026-11-04T10:01:41Z",
  "duration_seconds": 92,
  "billed_minutes": 2,
  "cost_inr": 12.0,
  "cost_credits": 24,
  "voice_tier": "sarvam",
  "language": "en-IN",
  "outcome": "confirmed",
  "sentiment": "positive",
  "summary": "Customer confirmed the order and delivery address.",
  "collected_data": { "address_ok": true },
  "recording_available": true,
  "failure_reason": null,
  "source": "api"
}

Call statuses

queuedAccepted and waiting for a free line.
ringingDialling the number.
in-progressAnswered; the conversation is under way.
completedAnswered and finished normally.
failedCould not be completed; see failure_reason.
no-answerRang but nobody picked up.
busyThe number was busy.
canceledEnded before it was answered, for example with the end endpoint.

A call usually moves queued, ringing, in-progress, then one of completed, failed, no-answer, busy or canceled. The last five are final.

Variables and metadata

Variables personalise one call. If the agent's greeting is "Hi {{customer_name}}, this is Priya", send variables {"customer_name": "Asha"} and the caller hears "Hi Asha, this is Priya". GET /v1/agents/{agent_id} lists the placeholders an agent uses in its variables array. business_name and agent_name are filled automatically.

Metadata is for you. Put your own ids there, for example {"order_id": "1042"}. It is stored, returned on the call and included in every webhook, and the AI never sees it.

Webhooks

Instead of polling, let us call you. Add a webhook URL and choose an event in Dashboard, Developers, Webhooks. Your endpoint must be publicly reachable over HTTPS.

call.completedAn answered call has ended.
call.failedA call was not answered, was busy or failed.
lead.qualifiedThe AI judged the person a qualified lead.
meeting.createdThe AI booked a meeting.

Example payload (plus the existing flat fields that older integrations rely on):

{
  "id": "evt_9f2c1a",
  "event": "call.completed",
  "created_at": "2026-11-04T10:01:42Z",
  "status": "completed",
  "agent_id": "agt_123",
  "campaign_id": "agt_123",
  "to": "+919876543210",
  "from": "+918012345678",
  "metadata": { "order_id": "1042" },
  "variables": { "customer_name": "Asha" },
  "recording_available": true,
  "cost_inr": 12.0,
  "failure_reason": null,
  "transcript": [
    { "role": "agent", "text": "Hi Asha, this is Priya from Acme Retail.", "timestamp": "2026-11-04T10:00:10Z" },
    { "role": "caller", "text": "Hello, yes.", "timestamp": "2026-11-04T10:00:14Z" }
  ]
}

Headers

X-NestVoice-EventThe event name.
X-NestVoice-DeliveryA unique id (uuid) for this delivery.
X-NestVoice-TimestampUnix seconds when it was sent.
X-NestVoice-Signature-256t=<timestamp>,v1=<hex HMAC-SHA256 of "<timestamp>.<raw body>" with your signing secret>.
X-NestVoice-SignatureLegacy: hex HMAC-SHA256 of the body only. Prefer Signature-256.

Verify the signature

Always verify before trusting a webhook. Use the raw request body exactly as received (do not re-serialise parsed JSON), compare in constant time, and reject timestamps older than 5 minutes to stop replays.

# Recompute the signature from the raw body you received and compare.
# TS = X-NestVoice-Timestamp, BODY = the exact request body bytes.
TS=1767225600
BODY='{"id":"evt_1","event":"call.completed"}'
printf '%s.%s' "$TS" "$BODY" \
  | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" -hex
# The hex digest must equal the v1= value in X-NestVoice-Signature-256.

Delivery and retries

Reply with any 2xx status within a few seconds. On a network error, 429 or 5xx we retry up to 3 attempts with exponential backoff. Every attempt is visible under the webhook in your dashboard, and you can send a test delivery from there. Make your handler idempotent: use the id field to ignore an event you already processed.

Interactive reference

Try every endpoint in your browser, or generate a client from the OpenAPI file.

Ready to start? Create an API key or contact support.