AI Voice Agent API
AI Voice Agent API
Have your AI voice agent call a customer from your own systems, pass the details it should use, and get the transcript, analysis and recording back.
- Base URL
- https://voice.agentive.co.in/api/public/v1
- Format
- JSON over HTTPS
- Auth
- x-api-key + x-api-secret
Introduction
You build an AI voice agent in the dashboard: its voice, its language, what it says first and what it should get done on the call. This API lets your own systems put that agent to work. When a lead fills a form, a payment is due or an order ships, your server asks the agent to call the customer, and hands it the details for that one call: the customer's name, the amount, the plan.
The agent speaks Hindi, English and Hinglish, the way you set it up. When the call ends you get the result back: what happened on the call, the conversation turn by turn, a short summary, the answers the agent collected and the recording. You can read these when you like, or have them pushed to your server the moment they are ready.
What you can do
- Place a call from an agent to one number, now.
- Trigger an AI voice agent campaign you saved in the dashboard, so its saved settings apply to the call.
- Fill the agent's greeting and prompt with your own values for each call, and attach your own ids that come back on every event.
- Read a call's status, transcript, analysis and recording, and receive them as signed webhooks.
- List your agents and numbers, and choose which agent answers a number.
charge_inr. See Billing.feature_disabled, the call reads show only AI calls (any other id answers 404), and account webhooks are sent for AI calls only. An account with voice broadcast, or with neither product, reads and receives events for all its calls. GET /account tells you what your key can use in its products object.Quick start
Three steps from keys to a real call on your own phone.
- Get your keysIn the Voice dashboard, open Developers > API Access and copy your publishable key and secret. The secret is shown in full only once. Put both in environment variables on your server, for example
AGENTIVE_KEYandAGENTIVE_SECRET. - Find your agent's idThe id is shown on the agent in the dashboard, next to its name. Or list your agents; use one where
can_call_outistrue, and note thevariablesit uses.cURLcurl https://voice.agentive.co.in/api/public/v1/agents \ -H "x-api-key: $AGENTIVE_KEY" \ -H "x-api-secret: $AGENTIVE_SECRET" - Place your first callCall your own mobile. The agent says the greeting, with
{{naam}}filled in.cURLcurl https://voice.agentive.co.in/api/public/v1/ai/calls \ -H "x-api-key: $AGENTIVE_KEY" \ -H "x-api-secret: $AGENTIVE_SECRET" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: first-ai-call-1" \ -d '{ "agent_id": 318, "to_number": "98XXXXXX21", "variables": { "naam": "Neha" }, "reference_id": "test-1" }'Keep theunique_idfrom the response and read the call when it ends:cURLcurl https://voice.agentive.co.in/api/public/v1/calls/b7e4c1d2-8f3a-4e6b-9c0d-2a1b3c4d5e6f \ -H "x-api-key: $AGENTIVE_KEY" \ -H "x-api-secret: $AGENTIVE_SECRET"
calling_hours. See Calling hours and consent.Platform conventions
Agentive ships two API families: the Voice APIs (Voice Broadcast, AI Voice Agent, Voice OTP and telephony webhooks) and the WhatsApp Business API. They share a brand, a dashboard account and a set of habits, but they are separate products and they do not agree on everything. This section is the one place that says what is common and, where they differ, exactly how.
Base URLs
| Voice APIs | WhatsApp Business API | |
|---|---|---|
| Base URL | https://voice.agentive.co.in/api/public/v1 | https://app.agentive.co.in/api/v1 |
| Covers | Voice Broadcast, AI Voice Agent, Voice OTP and the endpoints webhook events point back at. All of them share one base, one credential pair and one envelope. | Template sends for one API campaign. Issued and managed from the Chat dashboard. |
| Transport | HTTPS only, JSON in and JSON out. | HTTPS only, JSON in and JSON out. |
Authentication
| Voice APIs | WhatsApp Business API | |
|---|---|---|
| Credential | One publishable key (pk_live_...) and one secret (sk_live_...) per account. | One token (agcamp_...) per API campaign, so a leaked token can only send that campaign. |
| How to send it | x-api-key and x-api-secret headers, or HTTP Basic with the publishable key as the username and the secret as the password. | Authorization: Bearer <token>. |
| Scope | Account-wide. Every endpoint on the Voice base accepts it. | Campaign-wide. The template and number are fixed on the campaign. |
| Treat as | Both keys are confidential. The publishable key names your account to anyone who holds it, so keep it with the secret, on your server only. | A secret. Keep it on your server only. |
| Rotation | From the API Access tab of the Voice dashboard. The old secret stops working at once. | From the campaign page in the Chat dashboard. The old token stops working at once. |
Error envelope
| Voice APIs | WhatsApp Business API | |
|---|---|---|
| Shape | { status, code: number, error_code: string, error, details, unique_id, reference_id } | { error, code: string, details: object } |
| What to branch on | error_code. The numeric code mirrors the HTTP status and stays for compatibility. | code, which is itself the machine string. |
| Human sentence | details, repeated in error so a handler written for the WhatsApp shape reads a Voice error too. | error. |
| Extra context | Named fields beside the envelope (lines, lines_in_use, attempts_left, retry_after_sec, balance_inr). | A details object (missing, retryAfter, meta, validation). |
401 and 403
| Voice APIs | WhatsApp Business API | |
|---|---|---|
| 401 | Credentials missing, unknown, or rejected. The status never confirms whether a key exists. | Token missing or wrong. |
| 403 | Credentials accepted, but the account is not active or the product is not enabled. | Token accepted, but the campaign is not live or the number is not connected. |
| 404 | Not found, or not yours. A foreign id is never a 403, because that would confirm it exists. | Not found, or not yours. Same rule. |
Idempotency
| Voice APIs | WhatsApp Business API | |
|---|---|---|
| Key | Idempotency-Key request header, 1 to 128 visible characters. A longer key is refused with HTTP 400. | externalId in the body (the Idempotency-Key header is accepted too), up to 160 characters. |
| Window | 24 hours per account. | The life of the campaign. |
| A repeat | Replays the stored response. The one exception is a 5xx raised after the call was already handed to the network: that reply is stored as call_state_unknown with a sentence appended telling you to check the call before retrying. A 400 or 404 is stored for the 24 hours too, so once you fix the request or the dashboard setting, send it with a new key. | HTTP 200 with duplicate: true and the earlier delivery's current status. |
| Replay marker | Idempotent-Replayed: true, plus the original idempotency-replayed: true header. | duplicate: true in the body. |
| Same key, different body | HTTP 422 with idempotency_conflict. | The first send stands; the second is reported as a duplicate. |
| Still in flight | HTTP 409 with idempotency_in_progress and Retry-After. | Not applicable. |
Rate limits and headers
| Voice APIs | WhatsApp Business API | |
|---|---|---|
| Budget | Per account: 120 reads a minute and 120 writes a minute, counted separately, so polling can never starve call placement. | Per campaign token: 300 requests a minute over a sliding window. |
| Limit headers | RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset, with X-RateLimit-* aliases of the same three values. | None. |
| How long to wait | Retry-After on every 429 and every 409, mirrored in the body as retry_after_sec. | Retry-After on rate_limited; details.retryAfter (IST) on quiet_hours. |
| Capacity refusal | HTTP 429 concurrency_limit when the account has no free line, with lines, lines_in_use and retry_after_sec in the body and a Retry-After of 5 seconds. Call-placing responses also carry X-Agentive-Lines and X-Agentive-Lines-In-Use. | Not applicable; there are no lines to exhaust. |
Webhook signing
| Voice APIs | WhatsApp Business API | |
|---|---|---|
| Header to verify | X-Agentive-Signature-V2: t=<unix seconds>,v2=<hex> | X-Agentive-Signature: <hex>, with no prefix. |
| What is signed | HMAC-SHA256, with the signing secret, of t, a dot, then the exact raw body. | HMAC-SHA256 of the exact raw body with the endpoint's signing secret. |
| Freshness | Reject a t more than 300 seconds from your own clock. The timestamp is inside the signed material, so a captured delivery cannot be sent again later. | Nothing time-bound is signed. De-duplicate on the event id for good, and accept deliveries over HTTPS only. |
| Comparison | Constant time, on bytes. Reject on any mismatch, and reject a delivery with no version 2 header. | Constant time, on bytes. Reject on any mismatch. |
X-Agentive-Signature-V2. Voice deliveries also carry the older body-only X-Agentive-Signature for receivers built before version 2; do not accept it on its own, because it proves nothing about when a body was sent. The WhatsApp signature is the body alone with no prefix. A shared handler needs one verifier per product.Webhook delivery headers
| Voice APIs | WhatsApp Business API | |
|---|---|---|
| Event name | X-Agentive-Event | X-Agentive-Event |
| Event id | X-Agentive-Event-Id, the same value as id in the body. Shared by every endpoint that receives the event. | X-Agentive-Delivery, the same value as id in the body. Despite the name it is the event id, not a per-endpoint delivery id. |
| Delivery id | X-Agentive-Delivery-Id (dlv_...), one per event per endpoint, stable across every retry. | None. X-Agentive-Delivery is the Chat product's name for the event id above. |
| Attempt number | X-Agentive-Attempt, 1-based, counting through the whole retry schedule. | None. |
| Attempt time | X-Agentive-Timestamp, Unix seconds at the moment of this attempt, the same value as t in the version 2 signature. Check the age against the signed t, never against this header alone. | X-Agentive-Timestamp, the same timestamp as the envelope. |
| User agent | Agentive-Webhooks/2026-06-01 | Agentive-Webhooks/2026-05-08, the Chat payload version. |
Webhook retries
| Voice APIs | WhatsApp Business API | |
|---|---|---|
| Attempts | Nine: three fast, then six spread out. | Three. |
| Schedule | 0.5 s and 2 s between the first three, then 1 min, 5 min, 30 min, 2 h, 6 h and 12 h. | 0 s, 2 s and 8 s. |
| Total window | About 24 hours, after which the delivery is marked failed. | About 10 seconds. |
| Timeout per attempt | 6 seconds. | 10 seconds. |
| What you see | The dashboard lists recent deliveries per endpoint with the event, the latest result, the number of tries and the time in IST. | The campaign's Deliveries tab shows the message and its current status. |
Timestamps and amounts
| Voice APIs | WhatsApp Business API | |
|---|---|---|
| Time zone | Indian Standard Time, ISO 8601 with a +05:30 offset. The field is named per product: created_at_ist on a call, expires_at_ist on a verification, timestamp_ist on a webhook envelope. | timestamp on the webhook envelope is UTC ISO 8601. Schedules and quiet hours are set and reported in IST. |
| UTC alongside | created_at_utc on a call, and occurred_at (UTC) plus created (Unix seconds) on a webhook envelope. | The envelope timestamp is already UTC. |
| Money | Indian Rupees, always. balance_inr is a number, never a formatted string. | Indian Rupees, always. Amounts appear in the dashboard, not in the send API. |
Versioning and field case
| Voice APIs | WhatsApp Business API | |
|---|---|---|
| API version | In the path: /api/public/v1. | In the path: /api/v1. |
| Payload version | api_version on every webhook event. Currently 2026-06-01. | apiVersion on every webhook event. Currently 2026-05-08. |
| Compatibility | Fields are added, never removed or redefined. Ignore fields you do not recognise. | Fields are added, never removed or redefined. Ignore fields you do not recognise. |
| Field case | snake_case throughout, request and response. | camelCase throughout, request and response. |
Security
- Every credential is a secret. That includes the Voice publishable key, whatever its name suggests: treat it exactly like the secret. Keep all keys on your server, in an environment variable or a secrets store, never in a browser, a mobile app, a spreadsheet or source control.
- Send them only to the API. Never forward a key or a secret to any other host, and do not let an HTTP client carry them across a redirect to another host.
- Rotate at once if one leaks. Rotate the Voice pair from the API Access tab of the Voice dashboard and a WhatsApp token from its campaign page. The old value stops working immediately, so update your server in the same step.
- HTTPS only. Write
https://in the base URL in your code. A request sent over plain HTTP has already exposed its credentials before any redirect can protect it. - No address allow-listing today. Requests are not restricted to your server's IP addresses, and webhooks are not sent from a fixed list of addresses. Authenticate every webhook by its signature, never by where it came from.
Authentication
Every path on this page is relative to this base URL. Use https://; a request over plain HTTP exposes your keys before anything can protect them.
https://voice.agentive.co.in/api/public/v1Every request carries two keys from Developers > API Access in the Voice dashboard, sent as two headers:
Headers
| Header | Type | Required | Description |
|---|---|---|---|
| x-api-key | string | Required | Your publishable key ( pk_live_...). Treat it as confidential. See Authentication. |
| x-api-secret | string | Required | Your secret key ( sk_live_...). Server-side only. |
curl https://voice.agentive.co.in/api/public/v1/agents \
-H "x-api-key: $AGENTIVE_KEY" \
-H "x-api-secret: $AGENTIVE_SECRET"HTTP Basic authentication
You may send the same pair as HTTP Basic auth instead: the publishable key as the username and the secret as the password. When an x-api-key header is present it wins. Basic auth is what the download samples use, because an HTTP client never carries an Authorization header to another host.
curl https://voice.agentive.co.in/api/public/v1/agents \
-u "$AGENTIVE_KEY:$AGENTIVE_SECRET"- Treat the publishable key like the secret, despite its name. Keep both on your server, never in a web page, a mobile app or source control.
- The keys work for the whole account. The same pair can place calls and change which agent answers your numbers, so give it only to systems you trust with both.
- Send the keys only to
voice.agentive.co.in, never to any other host. - If a key leaks, rotate it from API Access at once. The old secret stops working immediately.
Authentication failures
| Situation | HTTP | error_code |
|---|---|---|
| No publishable key sent | 401 | missing_credentials |
| Unknown key, or a wrong secret | 401 | invalid_credentials |
| The account is not active | 403 | account_inactive |
| API access is not enabled for the account | 403 | api_disabled |
| The AI voice agent is not active on the account | 403 | feature_disabled |
401 always means the keys were rejected. 403 always means they were accepted and the account or product is not entitled. An unknown key and a wrong secret answer the same way, so a key cannot be probed.
Two ways to place a call
| A. POST /ai/calls | B. API campaign + trigger | |
|---|---|---|
| Best when | You want one agent to call one number, and your system decides everything else. | The campaign's saved settings should apply to every call: its calling number or pool, what happens when the customer agrees, WhatsApp after the call, and its reports. |
| Set up first | An agent that can place calls. | In Campaigns, create an API Campaign of type AI voice agent and pick the agent. |
| You send | agent_id, to_number, optionally from_number, variables, metadata. | number, optionally variables, metadata. |
| You get back | The dial id as unique_id. | run_<id> as unique_id, with the campaign and agent ids. |
| Shows in reports | Call History, marked as placed through the API. | Call History and the campaign's own reports, marked as placed through the API. |
Both lanes run the same agent, bill the same way, send the same webhooks and obey the same limits and calling hours.
Endpoints
| Endpoint | Purpose |
|---|---|
| POST /ai/calls | Have an agent call one number now. |
| POST /campaigns/{id}/trigger | Run a saved AI voice agent campaign for one number. |
| GET /calls/{unique_id} | Read a call: status, end reason, charge, metadata. |
| GET /calls/{unique_id}/transcript | The conversation, turn by turn. |
| GET /calls/{unique_id}/analysis | Summary, outcome and the fields the agent collected. |
| GET /calls/{unique_id}/recording | The call audio, streamed. |
| GET /agents | Your agents, with the variables each one uses. |
| GET /agents/{id} | One agent. |
| GET /numbers | Your numbers and the agent answering each. |
| PATCH /numbers/{number} | Put an agent on an inbound number, or take it off. |
Place an AI call
/ai/callsAsk an agent to call one Indian mobile number now. The call is queued at once and the response gives you its unique_id, the id every webhook event for this call carries. The agent says its greeting when the customer answers, with your variables filled in.
Headers
| Header | Type | Required | Description |
|---|---|---|---|
| x-api-key | string | Required | Your publishable key ( pk_live_...). Treat it as confidential. See Authentication. |
| x-api-secret | string | Required | Your secret key ( sk_live_...). Server-side only. |
| Content-Type | string | Required | Always application/json. |
| Idempotency-Key | string | Optional | A key unique to this logical request, 1 to 128 visible ASCII characters. A longer key is refused with HTTP 400. Send one on every call so a network retry can never place a second billed call. See Idempotency. Example: renewal-L-20931-1 |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| agent_id | number | Required | Which agent calls. It must be active and able to place calls ( can_call_out on List agents).Example: 318 |
| to_number | string | Required | The customer's Indian mobile number: 10 digits, with 91, or with +91. It must start with 6, 7, 8 or 9. Example: 98XXXXXX21 |
| from_number | string | Optional | The number the customer sees. It must be one of your numbers enabled for AI calls, free or attached to this agent. Leave it out and we choose one; see Which number the customer sees. Example: 011XXXXXX45 |
| variables | object | Optional | Values that fill the {{placeholders}} in the agent's greeting and prompt, for this call only. Up to 20 keys, strings or numbers, 200 characters each. Refused with 400 when a rule is broken. See Variables.Example: { "naam": "Neha", "amount": "₹2,499", "plan": "Gold" } |
| metadata | object | Optional | Your own data for this call. Never shown to the agent, never spoken. Echoed back on the call and on every webhook event. Up to 10 keys. See Metadata. Example: { "crm_lead_id": "L-20931" } |
| reference_id | string | Optional | Your own correlation key, up to 120 characters. Echoed on the response and on every event. It never de-duplicates anything; use an Idempotency-Key for that.Example: lead-5567 |
Request
curl https://voice.agentive.co.in/api/public/v1/ai/calls \
-H "x-api-key: $AGENTIVE_KEY" \
-H "x-api-secret: $AGENTIVE_SECRET" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: renewal-L-20931-1" \
-d '{
"agent_id": 318,
"to_number": "98XXXXXX21",
"variables": { "naam": "Neha", "amount": "₹2,499", "plan": "Gold" },
"metadata": { "crm_lead_id": "L-20931", "source": "website" },
"reference_id": "lead-5567"
}'Response
{
"status": "success",
"code": 200,
"unique_id": "b7e4c1d2-8f3a-4e6b-9c0d-2a1b3c4d5e6f",
"reference_id": "lead-5567",
"details": "Call queued.",
"call_id": "b7e4c1d2-8f3a-4e6b-9c0d-2a1b3c4d5e6f",
"agent_id": 318,
"to_number": "+9198XXXXXX21",
"from_number": "011XXXXXX45",
"call_status": "queued",
"metadata": { "crm_lead_id": "L-20931", "source": "website" }
}Response fields
| Field | Type | Description |
|---|---|---|
| unique_id | string | The call's id. Keep it: poll with it, and match webhooks on data.unique_id. |
| call_id | string | The same value as unique_id here. Once an answered call ends, the call record has its own call_id; this id stays the leg_id and unique_id never changes. |
| agent_id | number | The agent placing the call. |
| to_number | string | The customer, in +91 form. |
| from_number | string | The number the customer will see. |
| call_status | string | Always queued: accepted and on its way. A call with no free line or channel is refused, never parked. |
| reference_id | string | null | What you sent. |
| metadata | object | null | What you sent. A number comes back as text. |
Errors
| Status | Code | error_code | When | What to do |
|---|---|---|---|---|
| 400 | 400 | validation_error | agent_id is missing or not a positive whole number.details: "A valid agent_id is required." | |
| 400 | 400 | validation_error | to_number is not an Indian mobile number.details: "A valid Indian mobile to_number is required." | |
| 400 | 400 | validation_error | The agent is paused or in draft.details: "This agent is not active. Activate it in the dashboard first." | |
| 400 | 400 | validation_error | The agent is set to answer calls only.details: "This agent only takes inbound calls. Use an outbound-capable agent." | |
| 400 | 400 | validation_error | A variables or metadata rule is broken. The sentence names the key, for example "variables.amount must be at most 200 characters.", and the body adds field (here variables.amount). | Fix the value. See Variables and Metadata for the rules. |
| 400 | 400 | validation_error | You sent no from_number, and your account has no number enabled for AI calls and no calling pool.details: "No phone number is available on your account to place this call from." | Ask your account manager to enable a number for AI calls. |
| 400 | 400 | invalid_caller_id | from_number is not yours, or is not enabled for AI calls.details: "from_number must be a number on your account that is enabled for AI voice agents." | |
| 400 | 400 | invalid_caller_id | from_number is attached to a different AI agent.details: "from_number answers calls for another AI agent. Use a number that is free or attached to this agent." | |
| 404 | 404 | not_found | No agent with that id on your account.details: "Agent not found." | |
| 402 | 402 | insufficient_balance | Your wallet cannot cover one minute of this agent's per-minute price (and at least ₹5 on POST /ai/calls). The body adds balance_inr and minimum_balance_inr.details: "Account balance is too low to place this call. Please recharge." | Top up the wallet, then resend with the same Idempotency-Key. |
| 403 | 403 | feature_disabled | The AI voice agent, or outgoing AI calls, is not active on your account.details: "This feature is not enabled for your account. Please contact support." | Contact your account manager. |
| 403 | 403 | trial_restricted | A trial account may only call its registered number. | |
| 409 | 409 | calling_hours | Outside 9:00 AM to 9:00 PM IST. The sentence names the hours. | Queue the call and send it after 9:00 AM IST. |
| 409 | 409 | ai_concurrency_limit | Every AI channel on your account is busy, counting calls that are still ringing.details: "All your AI call channels are in use. Retry when a call finishes." | Retry-After is 5 seconds. |
| 409 | 409 | idempotency_in_progress | A request with the same Idempotency-Key is still running. | Wait for Retry-After (2 seconds) and resend with the same key. |
| 422 | 422 | idempotency_conflict | The same Idempotency-Key was sent with a different body. | Use a new key for a new request. |
| 429 | 429 | concurrency_limit | Every line on your account is in use. | Retry-After is 5 seconds. |
| 429 | 429 | number_flood | 3 API calls to this number in the last 10 minutes, or 10 today (IST). Counted across every API lane. Retry-After and retry_after_sec say how long: up to 600 seconds, or the seconds until midnight IST for the daily count. | Wait for Retry-After. Do not retry sooner. |
| 429 | 429 | daily_cap | The calling pool numbers you call from reached their daily limit. Retry-After and retry_after_sec count the seconds until midnight IST. | Wait for Retry-After (midnight IST). |
| 429 | 429 | rate_limited | Your per-minute request budget is spent. | Wait for Retry-After (up to 60 seconds). |
| 502 | 502 | call_state_unknown | We asked the network to place the call and lost track of it. It may or may not have gone out. | Check the call with GET /calls/{unique_id} or wait for a webhook before placing it again. Never retry blindly. |
| 503 | 503 | service_unavailable | Calling is briefly unavailable on our side, or your account's calling-hours setting could not be read, so the calling window could not be checked. In that case retry_after_sec is 30. Nothing was placed. | Retry after Retry-After with the same Idempotency-Key. |
{
"status": "error",
"code": 400,
"unique_id": null,
"reference_id": "lead-5567",
"details": "variables.amount must be at most 200 characters.",
"error_code": "validation_error",
"error": "variables.amount must be at most 200 characters.",
"field": "variables.amount"
}HTTP/1.1 409 Conflict
Retry-After: 5
{
"status": "error",
"code": 409,
"unique_id": null,
"reference_id": "lead-5567",
"details": "All your AI call channels are in use. Retry when a call finishes.",
"error_code": "ai_concurrency_limit",
"error": "All your AI call channels are in use. Retry when a call finishes.",
"retry_after_sec": 5
}Trigger an AI campaign
/campaigns/{id}/triggerRun an API campaign of type AI voice agent for one number. The call uses everything saved on the campaign: its agent, its calling number or pool, what happens when the customer agrees (connect to your team, note the interest or send a WhatsApp template), its ring time, voicemail handling and its webhook. Your request adds the number and, for this call only, variables and metadata. Find the campaign id on the campaign page or with List campaigns, where AI campaigns show type: "ai_agent" and their agent_id.
Headers
| Header | Type | Required | Description |
|---|---|---|---|
| x-api-key | string | Required | Your publishable key ( pk_live_...). Treat it as confidential. See Authentication. |
| x-api-secret | string | Required | Your secret key ( sk_live_...). Server-side only. |
| Content-Type | string | Required | Always application/json. |
| Idempotency-Key | string | Optional | A key unique to this logical request, 1 to 128 visible ASCII characters. A longer key is refused with HTTP 400. Send one on every call so a network retry can never place a second billed call. See Idempotency. Example: renewal-L-20931-1 |
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| id | number | Required | The API campaign's id. It must be on your account. Example: 57 |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| number | string | Required | The customer's Indian mobile number, in any of the forms above. Example: 98XXXXXX21 |
| variables | object | Optional | Values that fill the {{placeholders}} in the agent's greeting and prompt, for this call only. Up to 20 keys, strings or numbers, 200 characters each. Refused with 400 when a rule is broken. See Variables.Example: { "naam": "Neha", "amount": "₹2,499", "plan": "Gold" } |
| metadata | object | Optional | Your own data for this call. Never shown to the agent, never spoken. Echoed back on the call and on every webhook event. Up to 10 keys. See Metadata. Example: { "crm_lead_id": "L-20931" } |
| reference_id | string | Optional | Your own correlation key, up to 120 characters. Echoed on the response and on every event. It never de-duplicates anything; use an Idempotency-Key for that.Example: lead-5567 |
Request
curl https://voice.agentive.co.in/api/public/v1/campaigns/57/trigger \
-H "x-api-key: $AGENTIVE_KEY" \
-H "x-api-secret: $AGENTIVE_SECRET" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: renewal-L-20931-1" \
-d '{
"number": "98XXXXXX21",
"variables": { "naam": "Neha", "amount": "₹2,499", "plan": "Gold" },
"metadata": { "crm_lead_id": "L-20931" },
"reference_id": "lead-5567"
}'Response
{
"status": "success",
"code": 200,
"unique_id": "run_9150",
"reference_id": "lead-5567",
"details": "Call queued.",
"campaign_id": 57,
"agent_id": 318,
"call_status": "queued",
"metadata": { "crm_lead_id": "L-20931" }
}Response fields
| Field | Type | Description |
|---|---|---|
| unique_id | string | run_<id>: the call's id. Keep it: poll with it, and match webhooks on data.unique_id. |
| campaign_id | number | The API campaign you triggered. |
| agent_id | number | null | The agent the campaign calls with. |
| call_status | string | Always queued. |
| metadata | object | null | What you sent. A number comes back as text. |
Errors
| Status | Code | error_code | When | What to do |
|---|---|---|---|---|
| 400 | 400 | invalid_number | number is not an Indian mobile number.details: "A valid Indian mobile number is required." | |
| 400 | 400 | validation_error | The campaign's agent has been removed, is not active or only takes incoming calls. The sentence names the agent, or reads "This campaign has no active agent configured." | Attach an active agent that can place calls to the campaign in the dashboard. |
| 400 | 400 | validation_error | The campaign is paused or stopped in the dashboard, for example "That campaign is paused. Start it again in your dashboard before placing calls for it." | |
| 400 | 400 | validation_error | A variables or metadata rule is broken. The sentence names the key. | |
| 400 | 400 | invalid_caller_id | The campaign has no calling number and no calling pool, or the one it names can no longer be used for AI calls on your account. | Open the campaign and pick a number or a pool. |
| 400 | 400 | whatsapp_template_required | The campaign sends a WhatsApp message after the call and has no template saved. | |
| 403 | 403 | account_inactive | The account's calling is on hold. | Renew or contact your account manager. |
| 403 | 403 | whatsapp_locked | The campaign sends a WhatsApp message after the call and WhatsApp is not on your plan. | |
| 404 | 404 | not_found | No API campaign with that id on your account.details: "Campaign not found." | |
| 409 | 409 | whatsapp_not_connected | The campaign sends a WhatsApp message and no WhatsApp account is connected yet. | |
| 402 | 402 | insufficient_balance | Your wallet cannot cover one minute of this agent's per-minute price (and at least ₹5 on POST /ai/calls). The body adds balance_inr and minimum_balance_inr.details: "Account balance is too low to place this call. Please recharge." | Top up the wallet, then resend with the same Idempotency-Key. |
| 403 | 403 | feature_disabled | The AI voice agent, or outgoing AI calls, is not active on your account.details: "This feature is not enabled for your account. Please contact support." | Contact your account manager. |
| 403 | 403 | trial_restricted | A trial account may only call its registered number. | |
| 409 | 409 | calling_hours | Outside 9:00 AM to 9:00 PM IST. The sentence names the hours. | Queue the call and send it after 9:00 AM IST. |
| 409 | 409 | ai_concurrency_limit | Every AI channel on your account is busy, counting calls that are still ringing.details: "All your AI call channels are in use. Retry when a call finishes." | Retry-After is 5 seconds. |
| 409 | 409 | idempotency_in_progress | A request with the same Idempotency-Key is still running. | Wait for Retry-After (2 seconds) and resend with the same key. |
| 422 | 422 | idempotency_conflict | The same Idempotency-Key was sent with a different body. | Use a new key for a new request. |
| 429 | 429 | concurrency_limit | Every line on your account is in use. | Retry-After is 5 seconds. |
| 429 | 429 | number_flood | 3 API calls to this number in the last 10 minutes, or 10 today (IST). Counted across every API lane. Retry-After and retry_after_sec say how long: up to 600 seconds, or the seconds until midnight IST for the daily count. | Wait for Retry-After. Do not retry sooner. |
| 429 | 429 | rate_limited | Your per-minute request budget is spent. | Wait for Retry-After (up to 60 seconds). |
| 502 | 502 | call_state_unknown | We asked the network to place the call and lost track of it. It may or may not have gone out. | Check the call with GET /calls/{unique_id} or wait for a webhook before placing it again. Never retry blindly. |
| 503 | 503 | service_unavailable | Calling is briefly unavailable on our side, or your account's calling-hours setting could not be read, so the calling window could not be checked. In that case retry_after_sec is 30. Nothing was placed. | Retry after Retry-After with the same Idempotency-Key. |
| 502 | 502 | upstream_error | The call could not be started, and it was not placed.details: "Could not place the call right now. Please retry." | Retry with the same Idempotency-Key. |
variables with HTTP 400 and "This campaign type does not take variables.", and metadata with HTTP 400 and "This campaign type does not take metadata." Everything else about the trigger is described in the Voice Broadcast API.Get a call
/calls/{unique_id}Read where a call is and how it went. For a finished AI call the call object carries the end reason, the charge, your metadata and whether the transcript and analysis are ready. Poll no more than every few seconds; webhooks are the better way to wait.
Headers
| Header | Type | Required | Description |
|---|---|---|---|
| x-api-key | string | Required | Your publishable key ( pk_live_...). Treat it as confidential. See Authentication. |
| x-api-secret | string | Required | Your secret key ( sk_live_...). Server-side only. |
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| unique_id | string | Required | The unique_id you were given when you placed the call: the dial id from POST /ai/calls, or run_<id> from a trigger. The call_id and leg_id of the same call resolve too. Must belong to your account.Example: b7e4c1d2-8f3a-4e6b-9c0d-2a1b3c4d5e6f |
Request
curl https://voice.agentive.co.in/api/public/v1/calls/b7e4c1d2-8f3a-4e6b-9c0d-2a1b3c4d5e6f \
-H "x-api-key: $AGENTIVE_KEY" \
-H "x-api-secret: $AGENTIVE_SECRET"Response (a direct AI call, finished)
{
"status": "success",
"code": 200,
"unique_id": "b7e4c1d2-8f3a-4e6b-9c0d-2a1b3c4d5e6f",
"reference_id": "lead-5567",
"details": "completed",
"call_status": "completed",
"duration_sec": 94,
"call": {
"unique_id": "b7e4c1d2-8f3a-4e6b-9c0d-2a1b3c4d5e6f",
"call_id": "5d0c7a9e-3b21-4f68-a0d4-9e8b7c6a5f41",
"leg_id": "b7e4c1d2-8f3a-4e6b-9c0d-2a1b3c4d5e6f",
"direction": "outbound",
"from_number": "011XXXXXX45",
"to_number": "+9198XXXXXX21",
"status": "completed",
"end_reason": "completed",
"duration_sec": 94,
"campaign_id": null,
"reference_id": "lead-5567",
"created_at": "2026-09-29 05:12:08",
"created_at_ist": "2026-09-29T10:42:08+05:30",
"created_at_utc": "2026-09-29T05:12:08Z",
"agent_id": 318,
"via_api": true,
"metadata": { "crm_lead_id": "L-20931", "source": "website" },
"charge_inr": 7.52,
"transcript_available": true,
"analysis_available": true
},
"via_api": true,
"metadata": { "crm_lead_id": "L-20931", "source": "website" }
}Response fields
| Field | Type | Description |
|---|---|---|
| call.unique_id | string | The id the API gave you when you placed the call: the dial id from POST /ai/calls, or run_<id> for a trigger. The same value as the envelope's own unique_id and as data.unique_id on every webhook for the call. |
| call.call_id | string | Our internal id for the call record. Informational: match on unique_id. |
| call.leg_id | string | null | The dial leg's id, when the call record is keyed on something else. For a call from POST /ai/calls it is the dial id, from the first read to the last; null on a trigger. |
| call.direction | string | outbound for the calls this page places. |
| call.from_number | string | The number the customer saw. |
| call.to_number | string | The customer, in +91 form. |
| call.status | string | |
| call.end_reason | string | null | Why it ended, from the closed set. null while the call runs. |
| call.duration_sec | number | null | Talk time in seconds. |
| call.campaign_id | number | null | The run's id for a trigger, null for a direct call. |
| call.reference_id | string | null | What you sent. |
| call.created_at_ist | string | ISO 8601 at +05:30. created_at_utc carries the same instant in UTC. |
| call.agent_id | number | null | The AI agent on the call. |
| call.via_api | boolean | true when the call was placed through this API. |
| call.metadata | object | null | The metadata you sent. A number comes back as text. |
| call.charge_inr | number | null | The amount charged to your wallet for the call, in rupees, once it is billed. null until then. See Billing. |
| call.transcript_available | boolean | A transcript can be read with Get the transcript. |
| call.analysis_available | boolean | The analysis can be read with Get the analysis. |
Errors
| Status | Code | error_code | When | What to do |
|---|---|---|---|---|
| 404 | 404 | not_found | The id does not resolve to a call your API access can see. The sentence is "Call not found." for a run_<id> and "Not found." for any other id. | |
| 429 | 429 | rate_limited | Your per-minute read budget is spent. | Wait for Retry-After (up to 60 seconds). |
For a call from POST /ai/calls, unique_id is the dial id you were given, at the top and inside call, whichever of the call's ids you polled with; for a trigger, call.unique_id is run_<id>. via_api and metadata are repeated at the top level. For a run_<id> the response also carries the older call_status word, explained in the Voice Broadcast API. Read the call object instead. To page through many calls, use List calls. A call from POST /ai/calls that was never answered (missed, busy or failed) keeps answering its final status from our records after it is over, with duration_sec 0 and charge_inr 0; it does not turn into a 404.
Get the transcript
/calls/{unique_id}/transcriptThe conversation, turn by turn, as it was spoken: Hindi, English or Hinglish. Each turn has a role (agent or caller) and its text. Ready shortly after the call ends; the call object's transcript_available says when. The transcript inside call.analysed names the same speaker customer instead of caller.
Headers
| Header | Type | Required | Description |
|---|---|---|---|
| x-api-key | string | Required | Your publishable key ( pk_live_...). Treat it as confidential. See Authentication. |
| x-api-secret | string | Required | Your secret key ( sk_live_...). Server-side only. |
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| unique_id | string | Required | The unique_id you were given when you placed the call: the dial id from POST /ai/calls, or run_<id> from a trigger. The call_id and leg_id of the same call resolve too. Must belong to your account.Example: b7e4c1d2-8f3a-4e6b-9c0d-2a1b3c4d5e6f |
Request
curl https://voice.agentive.co.in/api/public/v1/calls/b7e4c1d2-8f3a-4e6b-9c0d-2a1b3c4d5e6f/transcript \
-H "x-api-key: $AGENTIVE_KEY" \
-H "x-api-secret: $AGENTIVE_SECRET"Response
{
"status": "success",
"code": 200,
"data": {
"unique_id": "b7e4c1d2-8f3a-4e6b-9c0d-2a1b3c4d5e6f",
"call_id": "5d0c7a9e-3b21-4f68-a0d4-9e8b7c6a5f41",
"leg_id": "b7e4c1d2-8f3a-4e6b-9c0d-2a1b3c4d5e6f",
"turns": [
{ "role": "agent", "text": "Namaste Neha ji, main Asha bol rahi hoon, aapke Gold plan ke renewal ke baare mein. Main ek automated assistant hoon." },
{ "role": "caller", "text": "Haan boliye." },
{ "role": "agent", "text": "Aapka renewal ₹2,499 ka hai. Kya main payment link WhatsApp par bhej doon?" },
{ "role": "caller", "text": "Haan, bhej dijiye." }
],
"created_at": "2026-09-29 05:13:44",
"created_at_ist": "2026-09-29T10:43:44+05:30"
}
}Errors
| Status | Code | error_code | When | What to do |
|---|---|---|---|---|
| 404 | 404 | not_found | The id does not resolve to a call your API access can see.details: "Call not found." | |
| 403 | 403 | feature_disabled | The AI voice agent is not active on your account. | |
| 429 | 429 | rate_limited | Your per-minute read budget is spent. | Wait for Retry-After (up to 60 seconds). |
| 404 | 404 | not_found | The call has no transcript yet, or never connected.details: "No transcript is available for this call." |
Get the analysis
/calls/{unique_id}/analysisWhat the call achieved: a short summary, the sentiment, the customer's intent, the outcome, tags, and the fields the agent was set up to collect (extractions). Generated shortly after the call ends; the call.analysed webhook brings the same result without polling.
Headers
| Header | Type | Required | Description |
|---|---|---|---|
| x-api-key | string | Required | Your publishable key ( pk_live_...). Treat it as confidential. See Authentication. |
| x-api-secret | string | Required | Your secret key ( sk_live_...). Server-side only. |
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| unique_id | string | Required | The unique_id you were given when you placed the call: the dial id from POST /ai/calls, or run_<id> from a trigger. The call_id and leg_id of the same call resolve too. Must belong to your account.Example: b7e4c1d2-8f3a-4e6b-9c0d-2a1b3c4d5e6f |
Request
curl https://voice.agentive.co.in/api/public/v1/calls/b7e4c1d2-8f3a-4e6b-9c0d-2a1b3c4d5e6f/analysis \
-H "x-api-key: $AGENTIVE_KEY" \
-H "x-api-secret: $AGENTIVE_SECRET"Response
{
"status": "success",
"code": 200,
"data": {
"unique_id": "b7e4c1d2-8f3a-4e6b-9c0d-2a1b3c4d5e6f",
"call_id": "5d0c7a9e-3b21-4f68-a0d4-9e8b7c6a5f41",
"leg_id": "b7e4c1d2-8f3a-4e6b-9c0d-2a1b3c4d5e6f",
"summary": "Neha agreed to renew the Gold plan and asked for the payment link on WhatsApp.",
"sentiment": "positive",
"sentiment_score": 0.7,
"intent": "renewal",
"outcome": "interested",
"extractions": { "renewal_confirmed": true, "preferred_channel": "WhatsApp" },
"tags": ["renewal", "payment-link"],
"created_at": "2026-09-29 05:14:02",
"created_at_ist": "2026-09-29T10:44:02+05:30"
}
}Response fields
| Field | Type | Description |
|---|---|---|
| data.summary | string | A few sentences in plain language. Empty when not set. |
| data.sentiment | string | null | positive, neutral or negative. |
| data.sentiment_score | number | null | From -1 (negative) to 1 (positive). |
| data.intent | string | null | What the customer wanted, in a few words. |
| data.outcome | string | null | Usually interested, not_interested, callback, do_not_call, voicemail or unclear. Treat a value you do not recognise as unclear. |
| data.extractions | object | The answers the agent collected, keyed by the field names set on the agent. Empty object when none. |
| data.tags | string[] | Labels for the call. Empty array when none. |
Errors
| Status | Code | error_code | When | What to do |
|---|---|---|---|---|
| 404 | 404 | not_found | The id does not resolve to a call your API access can see.details: "Call not found." | |
| 403 | 403 | feature_disabled | The AI voice agent is not active on your account. | |
| 429 | 429 | rate_limited | Your per-minute read budget is spent. | Wait for Retry-After (up to 60 seconds). |
| 404 | 404 | not_found | Not ready yet.details: "No analysis is available for this call yet. It is generated shortly after the call ends." | Wait for call.analysed, or try again in a minute. |
Download the recording
/calls/{unique_id}/recordingThe call audio. The API streams the file itself: HTTP 200 with the audio as the body, Content-Type audio/ogg (or audio/wav) and Accept-Ranges: bytes. It never redirects you to another address, so there is nothing to follow. A Range header gets HTTP 206 with that part of the file; a malformed range, several ranges or a range outside the file gets HTTP 416. Needs call recording on your account. Until a call has a recording the endpoint answers 404; the recording.available event says when it is ready.
Headers
| Header | Type | Required | Description |
|---|---|---|---|
| x-api-key | string | Required | Your publishable key ( pk_live_...). Treat it as confidential. See Authentication. |
| x-api-secret | string | Required | Your secret key ( sk_live_...). Server-side only. |
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| unique_id | string | Required | The unique_id you were given when you placed the call: the dial id from POST /ai/calls, or run_<id> from a trigger. The call_id and leg_id of the same call resolve too. Must belong to your account.Example: b7e4c1d2-8f3a-4e6b-9c0d-2a1b3c4d5e6f |
Request
# The API streams the audio itself. Basic auth: key as user, secret as password.
curl "https://voice.agentive.co.in/api/public/v1/calls/b7e4c1d2-8f3a-4e6b-9c0d-2a1b3c4d5e6f/recording" \
-u "$AGENTIVE_KEY:$AGENTIVE_SECRET" \
--output call.oggErrors
| Status | Code | error_code | When | What to do |
|---|---|---|---|---|
| 404 | 404 | not_found | No recording exists for the call, or not yet.details: "No recording is available for this call." | Wait for recording.available, or read recording_available on call.analysed. |
| 403 | 403 | feature_disabled | Call recording is not enabled for your account. | |
| 416 | 416 | validation_error | The Range header is malformed, names several ranges, or starts past the end of the file. Content-Range: bytes */<size> gives the size.details: "The requested byte range cannot be served. Send one range inside the file, or no Range header." | |
| 502 | 502 | upstream_error | The audio could not be fetched just now.details: "The recording is temporarily unavailable. Please retry." | Retry after a short pause. |
curl "https://voice.agentive.co.in/api/public/v1/calls/b7e4c1d2-8f3a-4e6b-9c0d-2a1b3c4d5e6f/recording" \
-u "$AGENTIVE_KEY:$AGENTIVE_SECRET" \
-H "Range: bytes=0-65535" \
-D - --output first-64k.ogg
# HTTP/1.1 206 Partial Content
# Content-Type: audio/ogg
# Accept-Ranges: bytes
# Content-Range: bytes 0-65535/412870x-api-key and x-api-secret instead, switch off redirect following for this request: most clients carry custom headers across a redirect, to wherever it points.List agents
/agentsYour AI voice agents, oldest first. Each one says whether it can place calls and take calls, and which variables its greeting and prompt use, so your code knows what to send. Agents are created and edited in the dashboard only.
Headers
| Header | Type | Required | Description |
|---|---|---|---|
| x-api-key | string | Required | Your publishable key ( pk_live_...). Treat it as confidential. See Authentication. |
| x-api-secret | string | Required | Your secret key ( sk_live_...). Server-side only. |
Request
curl https://voice.agentive.co.in/api/public/v1/agents \
-H "x-api-key: $AGENTIVE_KEY" \
-H "x-api-secret: $AGENTIVE_SECRET"Response
{
"status": "success",
"code": 200,
"data": [
{
"id": 318,
"name": "Renewal reminder (Asha)",
"status": "active",
"direction": "outbound",
"languages": ["hi", "en"],
"variables": [
{ "key": "naam", "default_set": true },
{ "key": "amount", "default_set": false },
{ "key": "plan", "default_set": true }
],
"can_call_out": true,
"can_take_calls": false
},
{
"id": 322,
"name": "Front desk",
"status": "active",
"direction": "inbound",
"languages": ["hi"],
"variables": [],
"can_call_out": false,
"can_take_calls": true
}
]
}Response fields
| Field | Type | Description |
|---|---|---|
| id | number | The agent_id to send. |
| name | string | The agent's name in the dashboard. |
| status | string | active, or another word when the agent is switched off; can_call_out and can_take_calls are then both false. |
| direction | string | inbound, outbound or both. |
| languages | string[] | The languages the agent speaks. |
| variables | object[] | Each {{key}} the agent's text uses, with default_set telling you whether the agent has a default for it. A key with no default should always be sent. |
| can_call_out | boolean | The agent can place calls through POST /ai/calls. |
| can_take_calls | boolean | The agent can be put on an inbound number. |
Errors
| Status | Code | error_code | When | What to do |
|---|---|---|---|---|
| 403 | 403 | feature_disabled | The AI voice agent is not active on your account. | |
| 429 | 429 | rate_limited | Your per-minute read budget is spent. | Wait for Retry-After (up to 60 seconds). |
Get an agent
/agents/{id}One agent, in the same shape as the list.
Headers
| Header | Type | Required | Description |
|---|---|---|---|
| x-api-key | string | Required | Your publishable key ( pk_live_...). Treat it as confidential. See Authentication. |
| x-api-secret | string | Required | Your secret key ( sk_live_...). Server-side only. |
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| id | number | Required | The agent's id. Example: 318 |
Response
{
"status": "success",
"code": 200,
"data": {
"id": 318,
"name": "Renewal reminder (Asha)",
"status": "active",
"direction": "outbound",
"languages": ["hi", "en"],
"variables": [
{ "key": "naam", "default_set": true },
{ "key": "amount", "default_set": false },
{ "key": "plan", "default_set": true }
],
"can_call_out": true,
"can_take_calls": false
}
}Errors
| Status | Code | error_code | When |
|---|---|---|---|
| 404 | 404 | not_found | No agent with that id on your account.details: "Agent not found." |
List numbers
/numbersYour own numbers and which agent answers each one. bindable is false when a number already rings your team, runs a call menu, is a shared line or is not enabled for AI, so an agent cannot be put on it through the API.
Headers
| Header | Type | Required | Description |
|---|---|---|---|
| x-api-key | string | Required | Your publishable key ( pk_live_...). Treat it as confidential. See Authentication. |
| x-api-secret | string | Required | Your secret key ( sk_live_...). Server-side only. |
Request
curl https://voice.agentive.co.in/api/public/v1/numbers \
-H "x-api-key: $AGENTIVE_KEY" \
-H "x-api-secret: $AGENTIVE_SECRET"Response
{
"status": "success",
"code": 200,
"data": [
{ "number": "011XXXXXX45", "inbound_agent_id": 322, "bindable": true },
{ "number": "079XXXXXX12", "inbound_agent_id": null, "bindable": false }
]
}Errors
| Status | Code | error_code | When | What to do |
|---|---|---|---|---|
| 403 | 403 | feature_disabled | The AI voice agent is not active on your account. | |
| 429 | 429 | rate_limited | Your per-minute read budget is spent. | Wait for Retry-After (up to 60 seconds). |
Put an agent on a number
/numbers/{number}Choose which agent answers calls to one of your numbers, or send null to take the agent off. The change applies to the next call to that number and is recorded in your account's security log.
Headers
| Header | Type | Required | Description |
|---|---|---|---|
| x-api-key | string | Required | Your publishable key ( pk_live_...). Treat it as confidential. See Authentication. |
| x-api-secret | string | Required | Your secret key ( sk_live_...). Server-side only. |
| Content-Type | string | Required | Always application/json. |
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| number | string | Required | One of your numbers, matched on its last ten digits. Example: 011XXXXXX45 |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| inbound_agent_id | number | null | Required | An active agent that can take calls, or null.Example: 322 |
Request
# Put agent 322 on the number. Send "inbound_agent_id": null to take it off.
curl -X PATCH https://voice.agentive.co.in/api/public/v1/numbers/011XXXXXX45 \
-H "x-api-key: $AGENTIVE_KEY" \
-H "x-api-secret: $AGENTIVE_SECRET" \
-H "Content-Type: application/json" \
-d '{ "inbound_agent_id": 322 }'Response
{
"status": "success",
"code": 200,
"data": { "number": "011XXXXXX45", "inbound_agent_id": 322 }
}Errors
| Status | Code | error_code | When | What to do |
|---|---|---|---|---|
| 400 | 400 | validation_error | inbound_agent_id is missing, the agent is not active or only places calls, or the number rings your team, runs a call menu, is a shared line or is not enabled for AI. The sentence says which. | Change it in the dashboard first, or ask your account manager to enable the number for AI. |
| 404 | 404 | not_found | The number or the agent is not on your account. |
Variables and metadata
Two optional objects travel with a call, and they do opposite jobs. variables are for the agent: they fill in what it says. metadata is for you: the agent never sees it, and it comes back on every event so you can match the result to your own records.
Variables
Write a placeholder such as {{naam}} in the agent's greeting or prompt in the dashboard. For each call, send the value in variables, and the agent uses it exactly where the placeholder is.
Greeting:
Namaste {{naam}} ji, main Asha bol rahi hoon. Main ek automated assistant hoon.
Prompt:
Customer ka {{plan}} plan renew hona hai. Renewal amount {{amount}} hai.
Payment link WhatsApp par bhejne ki permission lein.{
"variables": {
"naam": "Neha",
"amount": "₹2,499",
"plan": "Gold"
}
}Namaste Neha ji, main Asha bol rahi hoon. Main ek automated assistant hoon.Rules
| Rule | Detail |
|---|---|
| Shape | A JSON object with at most 20 keys. |
| Keys | Lowercased for you, then 1 to 40 characters of a-z, 0-9 and _. A key is matched to the agent's placeholders without regard to case, so naam fills {{Naam}} too. |
| Values | A string or a number (numbers are turned into text). At most 200 characters each after trimming, and at most 2,000 characters for all values together. Hindi and other scripts are fine. |
| Not allowed in a value | Line breaks and other control characters (tabs and the Unicode line and paragraph separators included), and the braces { and }, single or doubled. |
| Reserved names | company_name, brand_name, agent_name, caller_number, customer_number, current_date, current_time, current_day, current_weekday, call_id, org_id, agent_id, caller_phone, caller_phone_last4 and industry. The platform fills these itself, and a request that sends one is refused. |
| Unused keys | A key the agent's text does not use is ignored. |
| Missing keys | The agent uses its own default for that placeholder, if it has one (default_set on List agents). Send every key that has no default. |
A request that breaks a rule is refused as a whole with HTTP 400 and validation_error, and the sentence names the key, so a mistake never reaches a customer.
"₹2,499" rather than "2499.00 INR", a first name rather than a full legal name.Metadata
Attach your own ids and labels. They are stored with the call and returned on Get a call, on every call.* event, on call.analysed, on recording.available and on the agent's own ai_agent.* events. The agent never sees them and never says them.
{
"metadata": {
"crm_lead_id": "L-20931",
"source": "website",
"attempt": "2"
}
}- At most 10 keys.
- A key is 1 to 40 characters of letters, digits and
_, kept exactly as you sent it. - A value is a string or a number, at most 200 characters, with no control characters. A number is stored as text, so
2comes back as"2". - Do not put anything secret in metadata: it is echoed in every webhook.
Call lifecycle
Every AI call moves through the same few states. The webhook for each step is shown beside it.
| status | What it means | Webhook |
|---|---|---|
| queued | Accepted and being placed. | call.initiated |
| ringing | The customer's phone is ringing. | None for calls you place. |
| in_progress | The customer answered and is talking to the agent. | call.answered |
| completed | The call was answered and has ended. | call.completed |
| missed | It rang and nobody answered, or the line was busy or declined. | call.no_answer or call.busy |
| failed | The call could not be placed. | call.failed |
| (after the end) | The summary, outcome and transcript are ready. | call.analysed |
Exactly one terminal event is sent per call, including a call nobody answered. call.analysed follows an answered call once, usually within a minute of the end.
Status and end reasons
status and end_reason are closed sets. New values are not added without a new payload version, so you can write exhaustive handling with no open-ended fallback.
status (where the call is now)
| Value | Meaning | Webhook event |
|---|---|---|
| queued | Accepted and waiting to be placed. | call.initiated |
| ringing | The recipient's phone is ringing. | call.ringing |
| in_progress | The recipient answered and the call is connected. | call.answered |
| completed | The call finished after being answered. | call.completed |
| missed | The call reached the recipient but was never answered. | call.no_answer, call.busy |
| failed | The call could not be placed at all. | call.failed |
Transitions
| From | To | When |
|---|---|---|
| queued | ringing | We handed the call to the network and the recipient's phone started ringing. |
| ringing | in_progress | The recipient answered. |
| in_progress | completed | The call ended after being answered. end_reason is completed or voicemail. |
| queued, ringing | missed | It rang and was never answered. end_reason is no_answer, busy, rejected or canceled. |
| queued, ringing | failed | It could not be placed. end_reason is failed. |
completed, missed and failed are terminal. A call never leaves one of them, and end_reason is null until it reaches one.
end_reason (set on a terminal status)
| Value | Meaning | With status |
|---|---|---|
| completed | The call connected and ran to its normal close: the clip finished, the interactive flow ended, or either side hung up. | completed |
| voicemail | A voicemail system answered. It was detected and the call was closed. | completed |
| no_answer | It rang and the recipient never picked up. Safe to retry later. | missed |
| busy | The recipient was on another call. The number is reachable, so a retry can succeed. | missed |
| rejected | The recipient actively declined the call. | missed |
| canceled | The call was cancelled before it connected. | missed |
| failed | The call could not be placed, for example an invalid number or a network problem. | failed |
The simpler call_status on a run id
GET /calls/run_<id> keeps an older, smaller call_status vocabulary, unchanged so existing integrations keep working. Read it through this mapping, or read the call object on the same response, which carries the full set above.
| call_status | Means | Note |
|---|---|---|
| queued | queued | Same meaning. |
| ringing | ringing | Same meaning. |
| answered | in_progress | The same state under an older name. |
| completed | completed | Same meaning. |
| failed | missed or failed | This one value covers both. Read end_reason, or the call object's status, to tell a call that rang unanswered from one that could not be placed. |
end_reason values, not statuses. A call that rang unanswered and one that was busy both carry the status missed, and the reason is in end_reason. The webhook event name tells you the same thing without reading either field.On an AI call, end_reason is completed whenever the conversation happened, whoever hung up and however the agent closed the call. The analysis outcome tells you how it went.
Identifiers
Six identifiers move between your system and ours. Only one of them is the handle you poll and match on; the rest are for correlation, for fetching a specific artefact, or for retry safety.
The six ids
| Identifier | What it is | Where you get it | What you poll with | In webhooks |
|---|---|---|---|---|
| unique_id | The handle for the thing you just created. This is the id to keep. | The unique_id field of the response that placed the call or sent the code. | Yes. GET /calls/:unique_id. | data.unique_id on every call.* and recording.available event. |
| run id | The unique_id shape a broadcast or Interactive Voice call takes: run_<campaign_id>. | Returned by POST /calls (types audio_blast and press1) and by POST /campaigns/:id/trigger, including an AI voice agent campaign. | Yes. It is a unique_id. | data.unique_id. On a broadcast run its digits are data.campaign_id; on an AI voice agent run data.campaign_id is the campaign you triggered. |
| request_id | A verification request. The same value as that request's unique_id. The alias otp_id is accepted in a verify body. | Returned by POST /otp/send and by POST /calls with type: "otp". | Yes. GET /calls/:request_id. | data.request_id on every otp.* event. |
| call_id | Our record id for one call leg. Informational. | GET /calls, and the call object on GET /calls/:unique_id. | Not the id to poll with, though it resolves on GET /calls/:unique_id too. Use it for the transcript, analysis and recording of a specific leg. | data.call_id, null until the leg exists. |
| leg_id | A second id for the same call, present only when the record is keyed on something other than the phone leg. On a call placed with POST /ai/calls it is the dial id, the same value as unique_id. Added September 2026 beside an unchanged unique_id. | GET /calls, the call object on GET /calls/:unique_id, and the call and recording objects in webhooks. null on most calls. | Not the id to poll with, though it resolves on GET /calls/:unique_id too. | data.leg_id. |
| reference_id | Your own correlation key. We never interpret it and it never de-duplicates anything. | You send it. Up to 120 characters; control characters are stripped. | No. It is echoed, not addressable. | On the envelope as reference_id and again inside data. |
| Idempotency-Key | Your retry key for one logical request. 1 to 128 visible characters (a longer key is refused), stored for 24 hours. | You send it as a request header. | No. It addresses a stored response, not a resource. | Never sent. |
Ids we generate for delivery
| Identifier | What it is | Where you get it | What you poll with | In webhooks |
|---|---|---|---|---|
| event id | One webhook event (evt_...). The same value reaches every endpoint subscribed to it, and repeats across retries. | The event body's id (aliased event_id). | No. De-duplicate your handler on it. | id in the body and X-Agentive-Event-Id in the headers. |
| delivery id | One event on its way to one endpoint (dlv_...). Stable across every retry of that delivery. | The X-Agentive-Delivery-Id header on the delivery itself. | No. Record it in your own log, and quote it when you ask us about a specific delivery. | X-Agentive-Delivery-Id in the headers. |
POST /ai/calls has no run, so we return the dial id when you place it while the record itself is keyed on the conversation. That dial id is the call's unique_id on every webhook event and on every read: the call list, GET /calls/:unique_id, the transcript, the analysis and the recording. call_id is the record id, and leg_id is the dial id again. Either id resolves on GET /calls/:unique_id, so an id you stored earlier keeps working.unique_id the placing response gave you, and match webhooks on data.unique_id. Everything else is either yours (reference_id, Idempotency-Key) or ours to hand you for a specific artefact (call_id, event id, delivery id).Which number the customer sees
POST /ai/callswithfrom_number: that number, if it is yours, enabled for AI calls, and free or attached to this agent. A number attached to another AI agent is refused, so a customer who calls back always reaches the agent that called them. Otherwise the request is refused withinvalid_caller_id.POST /ai/callswithout it: a number of yours enabled for AI calls, preferring one already attached to the agent; if you have none, a number from the calling pool your account uses. If neither exists the request is refused with HTTP 400.- A trigger: exactly the number or calling pool saved on the campaign. A pool call goes out from one of the pool's numbers, paced like any campaign on that pool. When you create the API campaign you must choose a number enabled for AI calls (free or attached to the campaign's agent) or a pool; a campaign with neither is refused with
invalid_caller_id, and the call is never moved to some other number.
A number your team answers calls on is never used for an AI call.
Webhooks
Rather than polling, let the results come to you. AI calls send events to the account webhook endpoints you register under Developers > Webhooks in the Voice dashboard. Each delivery is signed, retried for about 24 hours until your endpoint answers with a 2xx, and carries an event id that repeats across retries. The envelope, headers and retry schedule are in Telephony Webhooks.
https:// address on port 443 or 8443. The same rule holds for every webhook address you give us, including an agent's own webhook and a campaign's, and it is checked on every delivery: an address that breaks it receives nothing. A custom signing secret must be at least 32 characters. An account can register up to 10 endpoints, each with the events it wants. Tick call.analysed to receive the AI results.via_api is true on the calls you placed. call.analysed is sent for every answered AI call, including incoming calls an agent answers. If your account has the AI voice agent and not voice broadcast, you receive events for AI calls only; any other account, including one with neither product, receives events for all its calls.Call events
An AI call placed through this API sends call.initiated, call.answered when the customer picks up, and exactly one terminal event. Every one carries the fields below in data, beside the usual call object.
data.unique_id is the id the API gave you, on every event of the call and on call.analysed: the dial id for POST /ai/calls, run_<id> for a trigger. call_id is our internal record id and can differ between the first events and the terminal one; leg_id carries the dial id on every event of a POST /ai/calls call.AI fields in data
| Field | Type | Description |
|---|---|---|
| unique_id | string | The id you were given when you placed the call: the dial id, or run_<id> for a trigger. The same on every event of the call and on call.analysed. Match on this. |
| call_id | string | null | Our internal id for the call record. It can change between the first events and the terminal one of the same call, so never match on it. |
| leg_id | string | null | The dial id for a call from POST /ai/calls, on every event of the call. Otherwise the phone leg's id when the call record is keyed on something else, else null. |
| status | string | Derived from the event name. |
| end_reason | string | null | Set on the terminal events. |
| duration_sec | number | null | Talk time, on the terminal events. |
| agent_id | number | The AI agent on the call. |
| campaign_id | number | null | The API campaign you triggered, or null for a direct call. |
| campaign_type | string | Always ai_agent here. |
| reference_id | string | null | What you sent. Also on the envelope. |
| metadata | object | null | What you sent. A number comes back as text. |
| via_api | boolean | true for calls placed through this API. |
| charge_inr | number | null | Terminal events only. What the call cost your wallet, in rupees; 0 for a call that never connected, null when an answered call is not billed yet (read it later with Get a call, or on call.analysed). |
call.initiated
{
"id": "evt_c3a91f0d7b2e4c6a8f1d5b3e9a7c2d40",
"object": "event",
"api_version": "2026-06-01",
"type": "call.initiated",
"created": 1790658728,
"occurred_at": "2026-09-29T05:12:08.000Z",
"timestamp_ist": "2026-09-29T10:42:08.000+05:30",
"org_id": 42,
"reference_id": "lead-5567",
"livemode": true,
"data": {
"object": "call",
"unique_id": "b7e4c1d2-8f3a-4e6b-9c0d-2a1b3c4d5e6f",
"call_id": "b7e4c1d2-8f3a-4e6b-9c0d-2a1b3c4d5e6f",
"leg_id": "b7e4c1d2-8f3a-4e6b-9c0d-2a1b3c4d5e6f",
"direction": "outbound",
"from_number": "011XXXXXX45",
"to_number": "+9198XXXXXX21",
"status": "queued",
"duration_sec": null,
"end_reason": null,
"campaign_id": null,
"campaign_type": "ai_agent",
"agent_id": 318,
"reference_id": "lead-5567",
"metadata": {
"crm_lead_id": "L-20931",
"source": "website"
},
"via_api": true,
"charge_inr": null,
"response": {
"disposition": null,
"digit": null,
"via": null,
"answers": null,
"voicemail_left": null
}
},
"event": "call.initiated",
"event_id": "evt_c3a91f0d7b2e4c6a8f1d5b3e9a7c2d40"
}call.answered
{
"id": "evt_d4b02a1e8c3f5d7b9a2e6c4f0b8d3e51",
"object": "event",
"api_version": "2026-06-01",
"type": "call.answered",
"created": 1790658741,
"occurred_at": "2026-09-29T05:12:21.000Z",
"timestamp_ist": "2026-09-29T10:42:21.000+05:30",
"org_id": 42,
"reference_id": "lead-5567",
"livemode": true,
"data": {
"object": "call",
"unique_id": "b7e4c1d2-8f3a-4e6b-9c0d-2a1b3c4d5e6f",
"call_id": "b7e4c1d2-8f3a-4e6b-9c0d-2a1b3c4d5e6f",
"leg_id": "b7e4c1d2-8f3a-4e6b-9c0d-2a1b3c4d5e6f",
"direction": "outbound",
"from_number": "011XXXXXX45",
"to_number": "+9198XXXXXX21",
"status": "in_progress",
"duration_sec": null,
"end_reason": null,
"campaign_id": null,
"campaign_type": "ai_agent",
"agent_id": 318,
"reference_id": "lead-5567",
"metadata": {
"crm_lead_id": "L-20931",
"source": "website"
},
"via_api": true,
"charge_inr": null,
"response": {
"disposition": null,
"digit": null,
"via": null,
"answers": null,
"voicemail_left": null
}
},
"event": "call.answered",
"event_id": "evt_d4b02a1e8c3f5d7b9a2e6c4f0b8d3e51"
}call.completed
charge_inr is what it cost.{
"id": "evt_e5c13b2f9d4a6e8c0b3f7d5a1c9e4f62",
"object": "event",
"api_version": "2026-06-01",
"type": "call.completed",
"created": 1790658836,
"occurred_at": "2026-09-29T05:13:56.000Z",
"timestamp_ist": "2026-09-29T10:43:56.000+05:30",
"org_id": 42,
"reference_id": "lead-5567",
"livemode": true,
"data": {
"object": "call",
"unique_id": "b7e4c1d2-8f3a-4e6b-9c0d-2a1b3c4d5e6f",
"call_id": "5d0c7a9e-3b21-4f68-a0d4-9e8b7c6a5f41",
"leg_id": "b7e4c1d2-8f3a-4e6b-9c0d-2a1b3c4d5e6f",
"direction": "outbound",
"from_number": "011XXXXXX45",
"to_number": "+9198XXXXXX21",
"status": "completed",
"duration_sec": 94,
"end_reason": "completed",
"campaign_id": null,
"campaign_type": "ai_agent",
"agent_id": 318,
"reference_id": "lead-5567",
"metadata": {
"crm_lead_id": "L-20931",
"source": "website"
},
"via_api": true,
"charge_inr": 7.52,
"response": {
"disposition": "interested",
"digit": null,
"via": "voice",
"answers": null,
"voicemail_left": null
}
},
"event": "call.completed",
"event_id": "evt_e5c13b2f9d4a6e8c0b3f7d5a1c9e4f62"
}call.no_answer
{
"id": "evt_f6d24c3a0e5b7f9d1c4a8e6b2d0f5a73",
"object": "event",
"api_version": "2026-06-01",
"type": "call.no_answer",
"created": 1790658812,
"occurred_at": "2026-09-29T05:13:32.000Z",
"timestamp_ist": "2026-09-29T10:43:32.000+05:30",
"org_id": 42,
"reference_id": "lead-5567",
"livemode": true,
"data": {
"object": "call",
"unique_id": "b7e4c1d2-8f3a-4e6b-9c0d-2a1b3c4d5e6f",
"call_id": "b7e4c1d2-8f3a-4e6b-9c0d-2a1b3c4d5e6f",
"leg_id": "b7e4c1d2-8f3a-4e6b-9c0d-2a1b3c4d5e6f",
"direction": "outbound",
"from_number": "011XXXXXX45",
"to_number": "+9198XXXXXX21",
"status": "missed",
"duration_sec": 0,
"end_reason": "no_answer",
"campaign_id": null,
"campaign_type": "ai_agent",
"agent_id": 318,
"reference_id": "lead-5567",
"metadata": {
"crm_lead_id": "L-20931",
"source": "website"
},
"via_api": true,
"charge_inr": 0,
"response": {
"disposition": null,
"digit": null,
"via": null,
"answers": null,
"voicemail_left": null
}
},
"event": "call.no_answer",
"event_id": "evt_f6d24c3a0e5b7f9d1c4a8e6b2d0f5a73"
}call.busy
{
"id": "evt_a7e35d4b1f6c8a0e2d5b9f7c3e1a6b84",
"object": "event",
"api_version": "2026-06-01",
"type": "call.busy",
"created": 1790658750,
"occurred_at": "2026-09-29T05:12:30.000Z",
"timestamp_ist": "2026-09-29T10:42:30.000+05:30",
"org_id": 42,
"reference_id": "lead-5567",
"livemode": true,
"data": {
"object": "call",
"unique_id": "b7e4c1d2-8f3a-4e6b-9c0d-2a1b3c4d5e6f",
"call_id": "b7e4c1d2-8f3a-4e6b-9c0d-2a1b3c4d5e6f",
"leg_id": "b7e4c1d2-8f3a-4e6b-9c0d-2a1b3c4d5e6f",
"direction": "outbound",
"from_number": "011XXXXXX45",
"to_number": "+9198XXXXXX21",
"status": "missed",
"duration_sec": 0,
"end_reason": "busy",
"campaign_id": null,
"campaign_type": "ai_agent",
"agent_id": 318,
"reference_id": "lead-5567",
"metadata": {
"crm_lead_id": "L-20931",
"source": "website"
},
"via_api": true,
"charge_inr": 0,
"response": {
"disposition": null,
"digit": null,
"via": null,
"answers": null,
"voicemail_left": null
}
},
"event": "call.busy",
"event_id": "evt_a7e35d4b1f6c8a0e2d5b9f7c3e1a6b84"
}call.failed
{
"id": "evt_b8f46e5c2a7d9b1f3e6c0a8d4f2b7c95",
"object": "event",
"api_version": "2026-06-01",
"type": "call.failed",
"created": 1790658733,
"occurred_at": "2026-09-29T05:12:13.000Z",
"timestamp_ist": "2026-09-29T10:42:13.000+05:30",
"org_id": 42,
"reference_id": "lead-5567",
"livemode": true,
"data": {
"object": "call",
"unique_id": "b7e4c1d2-8f3a-4e6b-9c0d-2a1b3c4d5e6f",
"call_id": "b7e4c1d2-8f3a-4e6b-9c0d-2a1b3c4d5e6f",
"leg_id": "b7e4c1d2-8f3a-4e6b-9c0d-2a1b3c4d5e6f",
"direction": "outbound",
"from_number": "011XXXXXX45",
"to_number": "+9198XXXXXX21",
"status": "failed",
"duration_sec": 0,
"end_reason": "failed",
"campaign_id": null,
"campaign_type": "ai_agent",
"agent_id": 318,
"reference_id": "lead-5567",
"metadata": {
"crm_lead_id": "L-20931",
"source": "website"
},
"via_api": true,
"charge_inr": 0,
"response": {
"disposition": null,
"digit": null,
"via": null,
"answers": null,
"voicemail_left": null
}
},
"event": "call.failed",
"event_id": "evt_b8f46e5c2a7d9b1f3e6c0a8d4f2b7c95"
}call.analysed
Fields in data
| Field | Type | Description |
|---|---|---|
| object | string | Always call_analysis. |
| unique_id | string | The id you were given when you placed the call: the dial id, or run_<id> for a trigger. The same as on the call events. |
| call_id | string | null | Our internal id for the call record. |
| leg_id | string | null | The dial id for a call from POST /ai/calls. Otherwise the phone leg's id when the call record is keyed on something else, else null. |
| direction | string | null | outbound for the calls you place; inbound when an agent answered an incoming call. |
| from_number, to_number | string | null | The number the customer saw, and the customer. |
| agent_id | number | null | The AI agent on the call. |
| campaign_id | number | null | The API campaign you triggered, or null for a direct call. |
| campaign_type | string | Always ai_agent. |
| via_api | boolean | true for calls placed through this API. |
| reference_id, metadata | string | null, object | null | What you sent. A metadata number comes back as text. |
| summary | string | null | A few sentences in plain language. |
| sentiment | string | null | positive, neutral or negative. |
| intent | string | null | What the customer wanted. |
| outcome | string | null | For example unclear, callback, interested, not_interested, do_not_call or voicemail. Treat a value you do not recognise as unclear. |
| interest | string | null | interested, not_interested or callback, when it was captured on the call; else null. |
| tags | string[] | Labels for the call. Empty array when none. |
| extractions | object | null | The answers the agent collected, keyed by the field names set on the agent. |
| transcript | object[] | The conversation: { role, text } with role agent or customer. |
| recording_available | boolean | The recording can be downloaded now. When false, wait for recording.available. |
| duration_sec | number | null | Talk time in seconds. |
| charge_inr | number | null | What the call cost your wallet, in rupees. null when it is not billed yet; Get a call has it later. |
{
"id": "evt_c9a57f6d3b8e0c2a4f7d1b9e5a3c8da6",
"object": "event",
"api_version": "2026-06-01",
"type": "call.analysed",
"created": 1790658854,
"occurred_at": "2026-09-29T05:14:14.000Z",
"timestamp_ist": "2026-09-29T10:44:14.000+05:30",
"org_id": 42,
"reference_id": "lead-5567",
"livemode": true,
"data": {
"object": "call_analysis",
"unique_id": "b7e4c1d2-8f3a-4e6b-9c0d-2a1b3c4d5e6f",
"call_id": "5d0c7a9e-3b21-4f68-a0d4-9e8b7c6a5f41",
"leg_id": "b7e4c1d2-8f3a-4e6b-9c0d-2a1b3c4d5e6f",
"direction": "outbound",
"from_number": "011XXXXXX45",
"to_number": "+9198XXXXXX21",
"agent_id": 318,
"campaign_id": null,
"campaign_type": "ai_agent",
"via_api": true,
"reference_id": "lead-5567",
"metadata": {
"crm_lead_id": "L-20931",
"source": "website"
},
"summary": "Neha agreed to renew the Gold plan and asked for the payment link on WhatsApp.",
"sentiment": "positive",
"intent": "renewal",
"outcome": "interested",
"interest": "interested",
"tags": [
"renewal",
"payment-link"
],
"extractions": {
"renewal_confirmed": true,
"preferred_channel": "WhatsApp"
},
"transcript": [
{
"role": "agent",
"text": "Namaste Neha ji, main Asha bol rahi hoon, aapke Gold plan ke renewal ke baare mein. Main ek automated assistant hoon."
},
{
"role": "customer",
"text": "Haan boliye."
},
{
"role": "agent",
"text": "Aapka renewal ₹2,499 ka hai. Kya main payment link WhatsApp par bhej doon?"
},
{
"role": "customer",
"text": "Haan, bhej dijiye."
}
],
"recording_available": true,
"duration_sec": 94,
"charge_inr": 7.52
},
"event": "call.analysed",
"event_id": "evt_c9a57f6d3b8e0c2a4f7d1b9e5a3c8da6"
}Agent webhooks
Separately, an agent can post its own events to an address set on the agent in the dashboard: ai_agent.call.started, ai_agent.call.ended, ai_agent.call.analysed, ai_agent.call.agent_ended, ai_agent.interest.captured, three ai_agent.transfer.* events and two ai_agent.whatsapp.* events. They cover every call the agent handles, not only API calls, and their call block carries your unique_id, reference_id and metadata. The address must be https:// on port 443 or 8443, like every webhook address.
{
"id": "evt_1f0e9d8c7b6a5f4e3d2c",
"type": "ai_agent.call.ended",
"event": "ai_agent.call.ended",
"created": 1790658836,
"timestamp_ist": "2026-09-29T10:43:56+05:30",
"test": false,
"data": {
"org_id": 42,
"occurred_at": "2026-09-29T05:13:56.000Z",
"agent": { "id": 318, "name": "Renewal reminder (Asha)" },
"call": {
"call_uuid": "5d0c7a9e-3b21-4f68-a0d4-9e8b7c6a5f41",
"unique_id": "b7e4c1d2-8f3a-4e6b-9c0d-2a1b3c4d5e6f",
"reference_id": "lead-5567",
"metadata": { "crm_lead_id": "L-20931", "source": "website" },
"direction": "outbound",
"customer_number": "+9198XXXXXX21",
"business_number": "+9111XXXXXX45",
"from_number": "+9111XXXXXX45",
"to_number": "+9198XXXXXX21",
"campaign": null,
"via_call_menu": null
},
"outcome": {
"status": "completed",
"end_reason": "customer_hangup",
"ended_by": "caller",
"duration_sec": 94,
"interest": "interested",
"transfer": null,
"whatsapp": null,
"details_complete": true
}
}
}They carry the same version 2 signature header (below), but with a different secret: the account's call-event signing secret, shown in the agent's Webhooks settings in the dashboard. It is the same secret that signs campaign webhooks and call menu key press webhooks, and it is not the per-endpoint secret under Developers > Webhooks. A receiver that gets both kinds of event must verify each one with its own secret.
ai_agent.call.ended and ai_agent.call.analysed are tried up to 4 times, about 2, 10 and 30 seconds apart; every other agent event is tried twice. For guaranteed delivery, use the account webhooks above, and call.analysed in particular: they keep retrying for about 24 hours.
Verifying a webhook
Verify every delivery before you trust it. Read the X-Agentive-Signature-V2 header, which looks like t=1790658854,v2=5f0c...:
- Read the raw bodyTake the body as bytes, before any JSON parsing.
- Check the timeReject the delivery if
tis more than 300 seconds from your own clock. - Compute the signatureHMAC-SHA256 with the signing secret for that webhook over
t, a dot, then the raw body, hex-encoded: the endpoint's own secret for an account webhook, the account's call-event secret for anai_agent.*event. - Compare safelyCompare it with
v2in constant time, after checking that the two are the same length in bytes. Answer 401 on any mismatch, and on a delivery with noX-Agentive-Signature-V2at all. - De-duplicateRecord the event
idand ignore one you have already handled: our retries repeat it.
import crypto from "node:crypto";
import express from "express";
const SECRET = process.env.AGENTIVE_WEBHOOK_SECRET;
if (!SECRET) throw new Error("AGENTIVE_WEBHOOK_SECRET is not set"); // never verify with an empty key
const TOLERANCE_SEC = 300; // five minutes
const app = express();
// The RAW body: the signature covers the exact bytes we sent.
app.use("/webhooks/agentive", express.raw({ type: "application/json" }));
function verifyV2(rawBody, header) {
const parts = {};
for (const piece of String(header || "").split(",")) {
const i = piece.indexOf("=");
if (i > 0) parts[piece.slice(0, i).trim()] = piece.slice(i + 1).trim();
}
const t = Number(parts.t);
if (!Number.isFinite(t) || !parts.v2) return false;
if (Math.abs(Math.floor(Date.now() / 1000) - t) > TOLERANCE_SEC) return false;
const expected = crypto
.createHmac("sha256", SECRET)
.update(String(t) + ".")
.update(rawBody)
.digest("hex");
const a = Buffer.from(parts.v2, "utf8");
const b = Buffer.from(expected, "utf8");
return a.length === b.length && crypto.timingSafeEqual(a, b); // compare byte lengths first
}
app.post("/webhooks/agentive", async (req, res) => {
if (!verifyV2(req.body, req.header("X-Agentive-Signature-V2"))) {
return res.status(401).send("bad signature");
}
const evt = JSON.parse(req.body.toString("utf8"));
// alreadyHandled / remember: your own store, for example a table keyed on the event id.
if (await alreadyHandled(evt.id)) return res.sendStatus(200); // our retries repeat the id
await remember(evt.id);
if (evt.type === "call.analysed") {
const d = evt.data;
console.log(d.metadata?.crm_lead_id, d.outcome, d.summary);
}
res.sendStatus(200);
});
app.listen(3000);X-Agentive-Signature that signs the body alone. Do not accept it on its own: it cannot tell a fresh delivery from one captured and sent again later. Never run a verifier with an empty secret; the samples refuse to start without one.To check your code by hand, compute a signature from the shell:
# AGENTIVE_WEBHOOK_SECRET must already be set in your environment.
T=1790658854
BODY='{"id":"evt_abc","type":"call.analysed","data":{}}'
printf '%s.%s' "$T" "$BODY" | python3 -c '
import hashlib, hmac, os, sys
key = os.environb[b"AGENTIVE_WEBHOOK_SECRET"]
print(hmac.new(key, sys.stdin.buffer.read(), hashlib.sha256).hexdigest())'
# Compare the result with the v2= part of X-Agentive-Signature-V2 (t=$T).Errors
Every error has the same envelope as the rest of the Voice APIs: the HTTP status, a numeric code that mirrors it, a stable error_code to branch on, and a sentence in details (repeated in error) for your logs.
| error_code | HTTP | Means | What to do |
|---|---|---|---|
| missing_credentials | 401 | No publishable key was sent. | Send both keys. |
| invalid_credentials | 401 | Unknown key, or a wrong secret. | Check the keys; rotate if unsure. |
| account_inactive | 403 | The account is not active, or its calling is on hold. | Contact your account manager. |
| feature_disabled | 403 | The AI voice agent, or the product this route needs, is not on the account. | Contact your account manager. |
| validation_error | 400, 413, 416 | A field is missing or breaks a rule (details names it), the body is over 64 KB, or a Range header cannot be served. | Fix the request. Do not retry unchanged. |
| invalid_number | 400 | The number is not an Indian mobile number. | Fix the number. |
| invalid_caller_id | 400 | The number to call from is not yours or not enabled for AI calls, or the campaign has none. | Pick another number, or fix the campaign. |
| insufficient_balance | 402 | The wallet cannot cover one minute of the call. | Top up, then resend with the same key. |
| trial_restricted | 403 | A trial account may only call its registered number. | Call your registered number, or move to a paid plan. |
| not_found | 404 | No such call, agent, number or campaign on your account. | Check the id. |
| calling_hours | 409 | Outside 9:00 AM to 9:00 PM IST. | Send it after 9:00 AM IST. |
| ai_concurrency_limit | 409 | Every AI channel is busy. | Retry after Retry-After (5 seconds). |
| idempotency_in_progress | 409 | The first request with this key is still running. | Retry after Retry-After with the same key. |
| idempotency_conflict | 422 | This key was used with a different body. | Use a new key. |
| rate_limited | 429 | Your request budget is spent. | Wait for Retry-After. |
| number_flood | 429 | Too many calls to this number (3 in 10 minutes, or 10 in a day). | Wait for Retry-After (up to 600 seconds, or until midnight IST). Do not retry sooner. |
| daily_cap | 429 | POST /ai/calls only: the calling pool numbers you call from reached their daily limit. | Wait for Retry-After (until midnight IST). |
| concurrency_limit | 429 | Every line on the account is in use. | Retry after Retry-After (5 seconds). |
| call_state_unknown | 502 | We may or may not have placed the call. | Check the call before placing it again. |
| upstream_error | 502 | We could not place the call, and it was not placed; or a recording could not be fetched. | Retry with the same key. |
| service_unavailable | 500, 503 | Briefly unavailable on our side, or (503, retry_after_sec 30) your calling-hours setting could not be read, so nothing was placed. | Retry after Retry-After with the same key. |
The full list, shared with the other Voice APIs, is in the Voice Broadcast API. Treat an error_code you do not recognise as a generic failure of its HTTP class.
Limits
Limits protect your customers and the phone network. Each one refuses with a clear error_code and a Retry-After header (mirrored as retry_after_sec in the body), so your code always knows how long to wait.
| Limit | Scope | error_code | Retry-After |
|---|---|---|---|
| 120 read requests a minute | Account | rate_limited | Up to 60 seconds. |
| 120 write requests a minute, counted apart from reads | Account | rate_limited | Up to 60 seconds. |
| Your lines: calls live at the same time | Account | concurrency_limit | 5 seconds. |
| Your AI channels: AI calls live at the same time, a smaller number inside your lines. Calls still ringing count. | Account | ai_concurrency_limit | 5 seconds (HTTP 409). |
| 3 calls to one number in 10 minutes, across every API lane | Account and number | number_flood | Up to 600 seconds. |
| 10 calls to one number in a day (IST), across every API lane | Account and number | number_flood | Until midnight IST. |
| A calling pool number's own daily limit, when POST /ai/calls picks a pool number | Calling pool | daily_cap | Until midnight IST. |
| 240 requests a minute refused for their keys (401 or 403) | Client address | rate_limited | Up to 60 seconds. Only refused requests count, so a working integration never meets it. |
| 20 wrong secrets a minute | Publishable key and client address | rate_limited | Up to 60 seconds. A correct secret is always let through. |
A request body over 64 KB is refused with HTTP 413 before it is read. Read your lines with Get account balance; call-placing responses also carry X-Agentive-Lines and X-Agentive-Lines-In-Use. A call with no free line or AI channel is refused, never parked, so queued always means the call is on its way.
ai_concurrency_limit as "wait 5 seconds", not as a failure.Calling hours and consent
- 9:00 AM to 9:00 PM IST, every day. An AI call requested outside those hours is refused with HTTP 409 and
calling_hours, and nothing is placed. If your account's calling-hours setting cannot be read, the call is refused too, never placed: HTTP 503 withservice_unavailableandretry_after_sec30, so retry after 30 seconds with the same key. Queue the request on your side and send it in the morning. Accounts approved for extended hours do not see this refusal. - Consent. Call only people who have agreed to hear from your business, and stop at once when someone asks you to. Keep your own record of who agreed and when.
- Say it is an automated assistant. Write it into the agent's greeting, as in the examples on this page, so the customer knows from the first sentence.
- Do not disturb. Numbers on the platform's do not disturb list are never called, whatever you send.
- Complaints. Complaints from people you call can lead to outgoing calls being switched off on your account. Calling people who did not ask to hear from you puts your number, and everyone else's, at risk.
Billing
- An answered AI call is charged to your wallet at the agent's per-minute price, in Indian Rupees, with a one-minute minimum and per-second billing after that.
- A call that is not answered, is busy or fails costs nothing.
- A call answered by a voicemail that the agent detects is charged at one tenth of the normal amount.
- Each finished call reports its cost in
charge_inr, on Get a call, on the terminal events and oncall.analysed. - Before a call is placed, your wallet must cover at least one minute of the agent's price (and at least ₹5 on
POST /ai/calls), or the request is refused with HTTP 402. Read your balance with Get account balance.
Idempotency
Send an Idempotency-Key header on every POST /ai/calls and every trigger. If the network drops and you are not sure whether the call went out, send the same request again with the same key: you get the first answer back, with Idempotent-Replayed: true, and no second call is placed.
- The key is 1 to 128 visible ASCII characters. A longer key is refused with HTTP 400.
- Keys are kept for 24 hours per account.
- The same key with a different body is refused with HTTP 422 and
idempotency_conflict. - A 400 or 404 answer is stored for the key for 24 hours too, and replayed however often you resend. After fixing the request, or the agent, campaign or number setting in the dashboard, send it with a new key.
- A 402, 403, 409 or 429 does not use the key up: fix the cause, then resend with the same key.
- A
call_state_unknownanswer is kept for the key. Check the call before you choose a new key and place it again.
The full rules are in the Voice Broadcast API; they are the same on every Voice endpoint.
Go-live checklist
Before your first real customer
- Both keys live in environment variables on your server, and nowhere else.
- Every call request sends an
Idempotency-Key, and acall_state_unknownanswer is checked before any new attempt. - Your code branches on
error_code, waits forRetry-Afteron 409 and 429, and queues requests that meetcalling_hours. - The agent's greeting says it is an automated assistant, and every placeholder it uses either has a default or is always sent in
variables. - You call only people who agreed to hear from you, and honour every request to stop.
- A webhook endpoint on
https://verifiesX-Agentive-Signature-V2with a five minute window, de-duplicates on the eventid, and answers 2xx quickly. - You match results on
data.unique_idor your ownmetadata, and readcall.analysedfor the outcome. - Your wallet has enough for the calls you plan, with a low-balance alert on your side.
- You placed a test call to your own number and saw every event arrive.
Next steps
Continue with the references that share this base URL and account.