Place AI calls, read transcripts and receive results from your own software with a simple REST API.
Built for builders
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.
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.
The key lacks a scope, the plan is not paid, or the number is on your do-not-call list.
not_found
404
That agent, call or recording does not exist in your workspace.
conflict
409
The call cannot be placed or changed right now (hours, lines busy, already ended, idempotency).
rate_limited
429
Too many requests; wait for Retry-After seconds.
server_error
500
Something went wrong on our side. Safe to retry with an Idempotency-Key.
Common codes
invalid_api_key
The key is unknown. Check for typos or extra spaces.
key_revoked
The key was revoked in the dashboard.
paid_plan_required
The API needs a paid plan.
missing_scope
The key does not have the scope this endpoint needs.
validation_error
A field failed validation; the message says which.
agent_not_found / call_not_found
No such agent or call in this workspace.
agent_cannot_dial
The agent is inbound-only and cannot place calls.
invalid_phone_number
to is not a dialable Indian number.
number_not_yours
from is not one of your connected numbers.
do_not_call
to is on your workspace do-not-call list.
outside_calling_hours
The agent's calling hours do not allow a call now.
insufficient_credits / plan_expired
Top up credits or renew the plan.
lines_busy and its variants
workspace_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_ended
The call is already over.
recording_not_available
This call has no recording.
idempotency_key_reused / idempotency_key_in_use
The same Idempotency-Key was used with a different body, or its first request is still running.
rate_limited
Slow 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.
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.
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.
{
"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*
string
Display name.
type
string
outbound (default), inbound or bidirectional.
prompt
string
Instructions for the AI. May contain {{placeholders}}.
greeting
string
First sentence spoken. May contain {{placeholders}}.
goal
string
What a successful call achieves.
language
string
Language code, for example en-IN or hi-IN.
voice_tier
string
Voice quality tier enabled for your workspace.
voice
string
Voice id from GET /v1/voices.
knowledge_base_ids
string[]
Knowledge bases the agent may use.
collected_fields
object[]
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.
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*
string
An outbound or bidirectional agent of your workspace.
to*
string
Indian number: +919876543210, 919876543210, 09876543210 or 9876543210. Normalised for you.
from
string
One of your own numbers. Default: the number the dashboard would pick.
variables
object
string 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.
metadata
object
Strings, 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.
Ended 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.completed
An answered call has ended.
call.failed
A call was not answered, was busy or failed.
lead.qualified
The AI judged the person a qualified lead.
meeting.created
The AI booked a meeting.
Example payload (plus the existing flat fields that older integrations rely on):
t=<timestamp>,v1=<hex HMAC-SHA256 of "<timestamp>.<raw body>" with your signing secret>.
X-NestVoice-Signature
Legacy: 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.