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.
Billing
AI calls are charged per minute to your wallet in Indian Rupees, at the agent's per-minute price, with a one-minute minimum on an answered call. A call that is never answered costs nothing. Every finished call reports what it cost in charge_inr. See Billing.
One base URL, one set of keys
This API shares its base URL, keys, response envelope, error codes and limits with the Voice Broadcast API. Call events arrive through the account webhooks described in Telephony Webhooks; the ones an AI call sends are listed on this page.
What your account needs
API access comes with the AI voice agent. If your account has the AI voice agent and not voice broadcast, the API offers only what is on this page: other call types are refused with HTTP 403 and 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.

  1. Get your keys
    In 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_KEY and AGENTIVE_SECRET.
  2. Find your agent's id
    The id is shown on the agent in the dashboard, next to its name. Or list your agents; use one where can_call_out is true, and note the variables it uses.
    cURL
    curl https://voice.agentive.co.in/api/public/v1/agents \
      -H "x-api-key: $AGENTIVE_KEY" \
      -H "x-api-secret: $AGENTIVE_SECRET"
  3. Place your first call
    Call your own mobile. The agent says the greeting, with {{naam}} filled in.
    cURL
    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: first-ai-call-1" \
      -d '{
        "agent_id": 318,
        "to_number": "98XXXXXX21",
        "variables": { "naam": "Neha" },
        "reference_id": "test-1"
      }'
    Keep the unique_id from the response and read the call when it ends:
    cURL
    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"
Between 9 am and 9 pm
Calls are placed only between 9:00 AM and 9:00 PM IST. A test outside those hours answers HTTP 409 with 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 APIsWhatsApp Business API
Base URLhttps://voice.agentive.co.in/api/public/v1https://app.agentive.co.in/api/v1
CoversVoice 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.
TransportHTTPS only, JSON in and JSON out.HTTPS only, JSON in and JSON out.

Authentication

Voice APIsWhatsApp Business API
CredentialOne 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 itx-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>.
ScopeAccount-wide. Every endpoint on the Voice base accepts it.Campaign-wide. The template and number are fixed on the campaign.
Treat asBoth 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.
RotationFrom 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 APIsWhatsApp Business API
Shape{ status, code: number, error_code: string, error, details, unique_id, reference_id }{ error, code: string, details: object }
What to branch onerror_code. The numeric code mirrors the HTTP status and stays for compatibility.code, which is itself the machine string.
Human sentencedetails, repeated in error so a handler written for the WhatsApp shape reads a Voice error too.error.
Extra contextNamed 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 APIsWhatsApp Business API
401Credentials missing, unknown, or rejected. The status never confirms whether a key exists.Token missing or wrong.
403Credentials 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.
404Not 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 APIsWhatsApp Business API
KeyIdempotency-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.
Window24 hours per account.The life of the campaign.
A repeatReplays 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 markerIdempotent-Replayed: true, plus the original idempotency-replayed: true header.duplicate: true in the body.
Same key, different bodyHTTP 422 with idempotency_conflict.The first send stands; the second is reported as a duplicate.
Still in flightHTTP 409 with idempotency_in_progress and Retry-After.Not applicable.

Rate limits and headers

Voice APIsWhatsApp Business API
BudgetPer 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 headersRateLimit-Limit, RateLimit-Remaining and RateLimit-Reset, with X-RateLimit-* aliases of the same three values.None.
How long to waitRetry-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 refusalHTTP 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 APIsWhatsApp Business API
Header to verifyX-Agentive-Signature-V2: t=<unix seconds>,v2=<hex>X-Agentive-Signature: <hex>, with no prefix.
What is signedHMAC-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.
FreshnessReject 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.
ComparisonConstant time, on bytes. Reject on any mismatch, and reject a delivery with no version 2 header.Constant time, on bytes. Reject on any mismatch.
Two different signatures
The Voice APIs sign the timestamp together with the body, and you verify only 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 APIsWhatsApp Business API
Event nameX-Agentive-EventX-Agentive-Event
Event idX-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 idX-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 numberX-Agentive-Attempt, 1-based, counting through the whole retry schedule.None.
Attempt timeX-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 agentAgentive-Webhooks/2026-06-01Agentive-Webhooks/2026-05-08, the Chat payload version.

Webhook retries

Voice APIsWhatsApp Business API
AttemptsNine: three fast, then six spread out.Three.
Schedule0.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 windowAbout 24 hours, after which the delivery is marked failed.About 10 seconds.
Timeout per attempt6 seconds.10 seconds.
What you seeThe 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 APIsWhatsApp Business API
Time zoneIndian 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 alongsidecreated_at_utc on a call, and occurred_at (UTC) plus created (Unix seconds) on a webhook envelope.The envelope timestamp is already UTC.
MoneyIndian 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 APIsWhatsApp Business API
API versionIn the path: /api/public/v1.In the path: /api/v1.
Payload versionapi_version on every webhook event. Currently 2026-06-01.apiVersion on every webhook event. Currently 2026-05-08.
CompatibilityFields 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 casesnake_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.
Report a vulnerability
If you find a security problem in the APIs, the webhooks or the dashboard, write to hello@agentive.co.in with the steps to reproduce it. Please do not test against accounts that are not yours. Our contact details are also published at /.well-known/security.txt.
Reading this as one platform
The habits that hold across both products: HTTPS only, JSON both ways, a stable machine code on every error, an idempotency key on every request a retry could repeat, HMAC-SHA256 on every webhook, Indian Rupees and Indian Standard Time. The habits that do not: the credential header, the envelope key names, the field case, the signature scheme and the retry schedule. Write one transport layer per product, then share everything above it.

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.

Base URL
https://voice.agentive.co.in/api/public/v1

Every request carries two keys from Developers > API Access in the Voice dashboard, sent as two headers:

Headers

HeaderTypeRequiredDescription
x-api-keystringRequired
Your publishable key (pk_live_...). Treat it as confidential. See Authentication.
x-api-secretstringRequired
Your secret key (sk_live_...). Server-side only.
cURL
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
curl https://voice.agentive.co.in/api/public/v1/agents \
  -u "$AGENTIVE_KEY:$AGENTIVE_SECRET"
Both keys are secrets
  • 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

SituationHTTPerror_code
No publishable key sent401missing_credentials
Unknown key, or a wrong secret401invalid_credentials
The account is not active403account_inactive
API access is not enabled for the account403api_disabled
The AI voice agent is not active on the account403feature_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/callsB. API campaign + trigger
Best whenYou 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 firstAn agent that can place calls.In Campaigns, create an API Campaign of type AI voice agent and pick the agent.
You sendagent_id, to_number, optionally from_number, variables, metadata.number, optionally variables, metadata.
You get backThe dial id as unique_id.run_<id> as unique_id, with the campaign and agent ids.
Shows in reportsCall 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

EndpointPurpose
POST /ai/callsHave an agent call one number now.
POST /campaigns/{id}/triggerRun 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}/transcriptThe conversation, turn by turn.
GET /calls/{unique_id}/analysisSummary, outcome and the fields the agent collected.
GET /calls/{unique_id}/recordingThe call audio, streamed.
GET /agentsYour agents, with the variables each one uses.
GET /agents/{id}One agent.
GET /numbersYour numbers and the agent answering each.
PATCH /numbers/{number}Put an agent on an inbound number, or take it off.

Place an AI call

POST/ai/calls

Ask 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

HeaderTypeRequiredDescription
x-api-keystringRequired
Your publishable key (pk_live_...). Treat it as confidential. See Authentication.
x-api-secretstringRequired
Your secret key (sk_live_...). Server-side only.
Content-TypestringRequired
Always application/json.
Idempotency-KeystringOptional
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

FieldTypeRequiredDescription
agent_idnumberRequired
Which agent calls. It must be active and able to place calls (can_call_out on List agents).
Example: 318
to_numberstringRequired
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_numberstringOptional
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
variablesobjectOptional
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" }
metadataobjectOptional
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_idstringOptional
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

JSON
{
  "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

FieldTypeDescription
unique_idstring
The call's id. Keep it: poll with it, and match webhooks on data.unique_id.
call_idstring
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_idnumber
The agent placing the call.
to_numberstring
The customer, in +91 form.
from_numberstring
The number the customer will see.
call_statusstring
Always queued: accepted and on its way. A call with no free line or channel is refused, never parked.
reference_idstring | null
What you sent.
metadataobject | null
What you sent. A number comes back as text.

Errors

StatusCodeerror_codeWhenWhat to do
400400validation_erroragent_id is missing or not a positive whole number.details: "A valid agent_id is required."
400400validation_errorto_number is not an Indian mobile number.details: "A valid Indian mobile to_number is required."
400400validation_errorThe agent is paused or in draft.details: "This agent is not active. Activate it in the dashboard first."
400400validation_errorThe agent is set to answer calls only.details: "This agent only takes inbound calls. Use an outbound-capable agent."
400400validation_errorA 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.
400400validation_errorYou 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.
400400invalid_caller_idfrom_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."
400400invalid_caller_idfrom_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."
404404not_foundNo agent with that id on your account.details: "Agent not found."
402402insufficient_balanceYour 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.
403403feature_disabledThe 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.
403403trial_restrictedA trial account may only call its registered number.
409409calling_hoursOutside 9:00 AM to 9:00 PM IST. The sentence names the hours.Queue the call and send it after 9:00 AM IST.
409409ai_concurrency_limitEvery 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.
409409idempotency_in_progressA request with the same Idempotency-Key is still running.Wait for Retry-After (2 seconds) and resend with the same key.
422422idempotency_conflictThe same Idempotency-Key was sent with a different body.Use a new key for a new request.
429429concurrency_limitEvery line on your account is in use.Retry-After is 5 seconds.
429429number_flood3 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.
429429daily_capThe 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).
429429rate_limitedYour per-minute request budget is spent.Wait for Retry-After (up to 60 seconds).
502502call_state_unknownWe 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.
503503service_unavailableCalling 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.
400 when a variable breaks a rule
{
  "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"
}
409 when every AI channel is busy
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

POST/campaigns/{id}/trigger

Run 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

HeaderTypeRequiredDescription
x-api-keystringRequired
Your publishable key (pk_live_...). Treat it as confidential. See Authentication.
x-api-secretstringRequired
Your secret key (sk_live_...). Server-side only.
Content-TypestringRequired
Always application/json.
Idempotency-KeystringOptional
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

ParameterTypeRequiredDescription
idnumberRequired
The API campaign's id. It must be on your account.
Example: 57

Request body

FieldTypeRequiredDescription
numberstringRequired
The customer's Indian mobile number, in any of the forms above.
Example: 98XXXXXX21
variablesobjectOptional
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" }
metadataobjectOptional
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_idstringOptional
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

JSON
{
  "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

FieldTypeDescription
unique_idstring
run_<id>: the call's id. Keep it: poll with it, and match webhooks on data.unique_id.
campaign_idnumber
The API campaign you triggered.
agent_idnumber | null
The agent the campaign calls with.
call_statusstring
Always queued.
metadataobject | null
What you sent. A number comes back as text.

Errors

StatusCodeerror_codeWhenWhat to do
400400invalid_numbernumber is not an Indian mobile number.details: "A valid Indian mobile number is required."
400400validation_errorThe 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.
400400validation_errorThe 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."
400400validation_errorA variables or metadata rule is broken. The sentence names the key.
400400invalid_caller_idThe 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.
400400whatsapp_template_requiredThe campaign sends a WhatsApp message after the call and has no template saved.
403403account_inactiveThe account's calling is on hold.Renew or contact your account manager.
403403whatsapp_lockedThe campaign sends a WhatsApp message after the call and WhatsApp is not on your plan.
404404not_foundNo API campaign with that id on your account.details: "Campaign not found."
409409whatsapp_not_connectedThe campaign sends a WhatsApp message and no WhatsApp account is connected yet.
402402insufficient_balanceYour 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.
403403feature_disabledThe 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.
403403trial_restrictedA trial account may only call its registered number.
409409calling_hoursOutside 9:00 AM to 9:00 PM IST. The sentence names the hours.Queue the call and send it after 9:00 AM IST.
409409ai_concurrency_limitEvery 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.
409409idempotency_in_progressA request with the same Idempotency-Key is still running.Wait for Retry-After (2 seconds) and resend with the same key.
422422idempotency_conflictThe same Idempotency-Key was sent with a different body.Use a new key for a new request.
429429concurrency_limitEvery line on your account is in use.Retry-After is 5 seconds.
429429number_flood3 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.
429429rate_limitedYour per-minute request budget is spent.Wait for Retry-After (up to 60 seconds).
502502call_state_unknownWe 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.
503503service_unavailableCalling 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.
502502upstream_errorThe 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 and metadata are for AI campaigns
A recorded-message or press-a-key campaign refuses 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

GET/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

HeaderTypeRequiredDescription
x-api-keystringRequired
Your publishable key (pk_live_...). Treat it as confidential. See Authentication.
x-api-secretstringRequired
Your secret key (sk_live_...). Server-side only.

Path parameters

ParameterTypeRequiredDescription
unique_idstringRequired
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)

JSON
{
  "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

FieldTypeDescription
call.unique_idstring
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_idstring
Our internal id for the call record. Informational: match on unique_id.
call.leg_idstring | 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.directionstring
outbound for the calls this page places.
call.from_numberstring
The number the customer saw.
call.to_numberstring
The customer, in +91 form.
call.statusstring
queued, ringing, in_progress, completed, missed or failed. See Status and end reasons.
call.end_reasonstring | null
Why it ended, from the closed set. null while the call runs.
call.duration_secnumber | null
Talk time in seconds.
call.campaign_idnumber | null
The run's id for a trigger, null for a direct call.
call.reference_idstring | null
What you sent.
call.created_at_iststring
ISO 8601 at +05:30. created_at_utc carries the same instant in UTC.
call.agent_idnumber | null
The AI agent on the call.
call.via_apiboolean
true when the call was placed through this API.
call.metadataobject | null
The metadata you sent. A number comes back as text.
call.charge_inrnumber | null
The amount charged to your wallet for the call, in rupees, once it is billed. null until then. See Billing.
call.transcript_availableboolean
A transcript can be read with Get the transcript.
call.analysis_availableboolean
The analysis can be read with Get the analysis.

Errors

StatusCodeerror_codeWhenWhat to do
404404not_foundThe 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.
429429rate_limitedYour 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

GET/calls/{unique_id}/transcript

The 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

HeaderTypeRequiredDescription
x-api-keystringRequired
Your publishable key (pk_live_...). Treat it as confidential. See Authentication.
x-api-secretstringRequired
Your secret key (sk_live_...). Server-side only.

Path parameters

ParameterTypeRequiredDescription
unique_idstringRequired
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

JSON
{
  "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

StatusCodeerror_codeWhenWhat to do
404404not_foundThe id does not resolve to a call your API access can see.details: "Call not found."
403403feature_disabledThe AI voice agent is not active on your account.
429429rate_limitedYour per-minute read budget is spent.Wait for Retry-After (up to 60 seconds).
404404not_foundThe call has no transcript yet, or never connected.details: "No transcript is available for this call."

Get the analysis

GET/calls/{unique_id}/analysis

What 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

HeaderTypeRequiredDescription
x-api-keystringRequired
Your publishable key (pk_live_...). Treat it as confidential. See Authentication.
x-api-secretstringRequired
Your secret key (sk_live_...). Server-side only.

Path parameters

ParameterTypeRequiredDescription
unique_idstringRequired
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

JSON
{
  "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

FieldTypeDescription
data.summarystring
A few sentences in plain language. Empty when not set.
data.sentimentstring | null
positive, neutral or negative.
data.sentiment_scorenumber | null
From -1 (negative) to 1 (positive).
data.intentstring | null
What the customer wanted, in a few words.
data.outcomestring | null
Usually interested, not_interested, callback, do_not_call, voicemail or unclear. Treat a value you do not recognise as unclear.
data.extractionsobject
The answers the agent collected, keyed by the field names set on the agent. Empty object when none.
data.tagsstring[]
Labels for the call. Empty array when none.

Errors

StatusCodeerror_codeWhenWhat to do
404404not_foundThe id does not resolve to a call your API access can see.details: "Call not found."
403403feature_disabledThe AI voice agent is not active on your account.
429429rate_limitedYour per-minute read budget is spent.Wait for Retry-After (up to 60 seconds).
404404not_foundNot 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

GET/calls/{unique_id}/recording

The 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

HeaderTypeRequiredDescription
x-api-keystringRequired
Your publishable key (pk_live_...). Treat it as confidential. See Authentication.
x-api-secretstringRequired
Your secret key (sk_live_...). Server-side only.

Path parameters

ParameterTypeRequiredDescription
unique_idstringRequired
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.ogg

Errors

StatusCodeerror_codeWhenWhat to do
404404not_foundNo 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.
403403feature_disabledCall recording is not enabled for your account.
416416validation_errorThe 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."
502502upstream_errorThe audio could not be fetched just now.details: "The recording is temporarily unavailable. Please retry."Retry after a short pause.
A byte range
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/412870
Never send your secret to another host
The samples use HTTP Basic, which HTTP clients never forward to a different host. If you send x-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

GET/agents

Your 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

HeaderTypeRequiredDescription
x-api-keystringRequired
Your publishable key (pk_live_...). Treat it as confidential. See Authentication.
x-api-secretstringRequired
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

JSON
{
  "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

FieldTypeDescription
idnumber
The agent_id to send.
namestring
The agent's name in the dashboard.
statusstring
active, or another word when the agent is switched off; can_call_out and can_take_calls are then both false.
directionstring
inbound, outbound or both.
languagesstring[]
The languages the agent speaks.
variablesobject[]
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_outboolean
The agent can place calls through POST /ai/calls.
can_take_callsboolean
The agent can be put on an inbound number.

Errors

StatusCodeerror_codeWhenWhat to do
403403feature_disabledThe AI voice agent is not active on your account.
429429rate_limitedYour per-minute read budget is spent.Wait for Retry-After (up to 60 seconds).

Get an agent

GET/agents/{id}

One agent, in the same shape as the list.

Headers

HeaderTypeRequiredDescription
x-api-keystringRequired
Your publishable key (pk_live_...). Treat it as confidential. See Authentication.
x-api-secretstringRequired
Your secret key (sk_live_...). Server-side only.

Path parameters

ParameterTypeRequiredDescription
idnumberRequired
The agent's id.
Example: 318

Response

JSON
{
  "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

StatusCodeerror_codeWhen
404404not_foundNo agent with that id on your account.details: "Agent not found."

List numbers

GET/numbers

Your 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

HeaderTypeRequiredDescription
x-api-keystringRequired
Your publishable key (pk_live_...). Treat it as confidential. See Authentication.
x-api-secretstringRequired
Your secret key (sk_live_...). Server-side only.

Request

cURL
curl https://voice.agentive.co.in/api/public/v1/numbers \
  -H "x-api-key: $AGENTIVE_KEY" \
  -H "x-api-secret: $AGENTIVE_SECRET"

Response

JSON
{
  "status": "success",
  "code": 200,
  "data": [
    { "number": "011XXXXXX45", "inbound_agent_id": 322, "bindable": true },
    { "number": "079XXXXXX12", "inbound_agent_id": null, "bindable": false }
  ]
}

Errors

StatusCodeerror_codeWhenWhat to do
403403feature_disabledThe AI voice agent is not active on your account.
429429rate_limitedYour per-minute read budget is spent.Wait for Retry-After (up to 60 seconds).

Put an agent on a number

PATCH/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

HeaderTypeRequiredDescription
x-api-keystringRequired
Your publishable key (pk_live_...). Treat it as confidential. See Authentication.
x-api-secretstringRequired
Your secret key (sk_live_...). Server-side only.
Content-TypestringRequired
Always application/json.

Path parameters

ParameterTypeRequiredDescription
numberstringRequired
One of your numbers, matched on its last ten digits.
Example: 011XXXXXX45

Request body

FieldTypeRequiredDescription
inbound_agent_idnumber | nullRequired
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

JSON
{
  "status": "success",
  "code": 200,
  "data": { "number": "011XXXXXX45", "inbound_agent_id": 322 }
}

Errors

StatusCodeerror_codeWhenWhat to do
400400validation_errorinbound_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.
404404not_foundThe number or the agent is not on your account.
This changes who answers your customers
Anyone holding your keys can move your numbers between agents. Keep the keys on systems you trust, and watch the security log.

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.

In the agent (dashboard)
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.
In your request
{
  "variables": {
    "naam": "Neha",
    "amount": "₹2,499",
    "plan": "Gold"
  }
}
What the customer hears
Namaste Neha ji, main Asha bol rahi hoon. Main ek automated assistant hoon.

Rules

RuleDetail
ShapeA JSON object with at most 20 keys.
KeysLowercased 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.
ValuesA 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 valueLine breaks and other control characters (tabs and the Unicode line and paragraph separators included), and the braces { and }, single or doubled.
Reserved namescompany_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 keysA key the agent's text does not use is ignored.
Missing keysThe 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.

Values become part of the agent's instructions
A value is inserted into the agent's greeting and prompt exactly as you wrote it, and the agent reads it as part of its instructions. Pass facts your own systems control, such as a name, an amount, a date or a plan name. Never pass free text typed by the person being called, such as a form comment or a chat message.
Keep values short and speakable
Write values the way the agent should say them: "₹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.

In your request
{
  "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 2 comes 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.

queuedringingin_progresscompleted
From queued or ringing, it can instead end asmissedorfailed
statusWhat it meansWebhook
queuedAccepted and being placed.call.initiated
ringingThe customer's phone is ringing.None for calls you place.
in_progressThe customer answered and is talking to the agent.call.answered
completedThe call was answered and has ended.call.completed
missedIt rang and nobody answered, or the line was busy or declined.call.no_answer or call.busy
failedThe 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)

ValueMeaningWebhook event
queuedAccepted and waiting to be placed.call.initiated
ringingThe recipient's phone is ringing.call.ringing
in_progressThe recipient answered and the call is connected.call.answered
completedThe call finished after being answered.call.completed
missedThe call reached the recipient but was never answered.call.no_answer, call.busy
failedThe call could not be placed at all.call.failed

Transitions

FromToWhen
queuedringingWe handed the call to the network and the recipient's phone started ringing.
ringingin_progressThe recipient answered.
in_progresscompletedThe call ended after being answered. end_reason is completed or voicemail.
queued, ringingmissedIt rang and was never answered. end_reason is no_answer, busy, rejected or canceled.
queued, ringingfailedIt 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)

ValueMeaningWith status
completedThe call connected and ran to its normal close: the clip finished, the interactive flow ended, or either side hung up.completed
voicemailA voicemail system answered. It was detected and the call was closed.completed
no_answerIt rang and the recipient never picked up. Safe to retry later.missed
busyThe recipient was on another call. The number is reachable, so a retry can succeed.missed
rejectedThe recipient actively declined the call.missed
canceledThe call was cancelled before it connected.missed
failedThe 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_statusMeansNote
queuedqueuedSame meaning.
ringingringingSame meaning.
answeredin_progressThe same state under an older name.
completedcompletedSame meaning.
failedmissed or failedThis 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.
No status called no_answer or busy
Those are 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

IdentifierWhat it isWhere you get itWhat you poll withIn webhooks
unique_idThe 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 idThe 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_idA 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_idOur 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_idA 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_idYour 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-KeyYour 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

IdentifierWhat it isWhere you get itWhat you poll withIn webhooks
event idOne 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 idOne 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.
Some calls carry two ids
A call placed with 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.
One rule to remember
Poll with the 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/calls with from_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 with invalid_caller_id.
  • POST /ai/calls without 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.

Registering an endpoint
An endpoint must be an 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.
Which calls send events
Account webhooks describe calls on your account, not only the ones you place through this API: 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.

Match on unique_id
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

FieldTypeDescription
unique_idstring
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_idstring | 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_idstring | 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.
statusstring
Derived from the event name.
end_reasonstring | null
Set on the terminal events.
duration_secnumber | null
Talk time, on the terminal events.
agent_idnumber
The AI agent on the call.
campaign_idnumber | null
The API campaign you triggered, or null for a direct call.
campaign_typestring
Always ai_agent here.
reference_idstring | null
What you sent. Also on the envelope.
metadataobject | null
What you sent. A number comes back as text.
via_apiboolean
true for calls placed through this API.
charge_inrnumber | 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).
Webhook event

call.initiated

Fires whenThe call is accepted and queued.
Sample event
{
  "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"
}
Webhook event

call.answered

Fires whenThe customer answers and the agent starts talking.
Sample event
{
  "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"
}
Webhook event

call.completed

Fires whenAn answered call ends. charge_inr is what it cost.
Sample event
{
  "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"
}
Webhook event

call.no_answer

Fires whenThe phone rang and nobody picked up. Safe to try again later, within the limits.
Sample event
{
  "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"
}
Webhook event

call.busy

Fires whenThe line was busy or the customer declined.
Sample event
{
  "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"
}
Webhook event

call.failed

Fires whenThe call could not be placed, for example an unreachable number.
Sample event
{
  "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"
}
Webhook event

call.analysed

Fires whenOnce per answered AI call, after the post-call analysis is ready. This is the event to build on: it carries the summary, the outcome, the collected answers and the transcript in one delivery.

Fields in data

FieldTypeDescription
objectstring
Always call_analysis.
unique_idstring
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_idstring | null
Our internal id for the call record.
leg_idstring | 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.
directionstring | null
outbound for the calls you place; inbound when an agent answered an incoming call.
from_number, to_numberstring | null
The number the customer saw, and the customer.
agent_idnumber | null
The AI agent on the call.
campaign_idnumber | null
The API campaign you triggered, or null for a direct call.
campaign_typestring
Always ai_agent.
via_apiboolean
true for calls placed through this API.
reference_id, metadatastring | null, object | null
What you sent. A metadata number comes back as text.
summarystring | null
A few sentences in plain language.
sentimentstring | null
positive, neutral or negative.
intentstring | null
What the customer wanted.
outcomestring | null
For example unclear, callback, interested, not_interested, do_not_call or voicemail. Treat a value you do not recognise as unclear.
intereststring | null
interested, not_interested or callback, when it was captured on the call; else null.
tagsstring[]
Labels for the call. Empty array when none.
extractionsobject | null
The answers the agent collected, keyed by the field names set on the agent.
transcriptobject[]
The conversation: { role, text } with role agent or customer.
recording_availableboolean
The recording can be downloaded now. When false, wait for recording.available.
duration_secnumber | null
Talk time in seconds.
charge_inrnumber | null
What the call cost your wallet, in rupees. null when it is not billed yet; Get a call has it later.
Sample event
{
  "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"
}
Missed it?
The same result is always available from Get the analysis and Get the transcript.

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.

ai_agent.call.ended (shortened)
{
  "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...:

  1. Read the raw body
    Take the body as bytes, before any JSON parsing.
  2. Check the time
    Reject the delivery if t is more than 300 seconds from your own clock.
  3. Compute the signature
    HMAC-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 an ai_agent.* event.
  4. Compare safely
    Compare it with v2 in constant time, after checking that the two are the same length in bytes. Answer 401 on any mismatch, and on a delivery with no X-Agentive-Signature-V2 at all.
  5. De-duplicate
    Record the event id and 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);
Only version 2
Deliveries also carry an older 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:

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_codeHTTPMeansWhat to do
missing_credentials401No publishable key was sent.Send both keys.
invalid_credentials401Unknown key, or a wrong secret.Check the keys; rotate if unsure.
account_inactive403The account is not active, or its calling is on hold.Contact your account manager.
feature_disabled403The AI voice agent, or the product this route needs, is not on the account.Contact your account manager.
validation_error400, 413, 416A 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_number400The number is not an Indian mobile number.Fix the number.
invalid_caller_id400The 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_balance402The wallet cannot cover one minute of the call.Top up, then resend with the same key.
trial_restricted403A trial account may only call its registered number.Call your registered number, or move to a paid plan.
not_found404No such call, agent, number or campaign on your account.Check the id.
calling_hours409Outside 9:00 AM to 9:00 PM IST.Send it after 9:00 AM IST.
ai_concurrency_limit409Every AI channel is busy.Retry after Retry-After (5 seconds).
idempotency_in_progress409The first request with this key is still running.Retry after Retry-After with the same key.
idempotency_conflict422This key was used with a different body.Use a new key.
rate_limited429Your request budget is spent.Wait for Retry-After.
number_flood429Too 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_cap429POST /ai/calls only: the calling pool numbers you call from reached their daily limit.Wait for Retry-After (until midnight IST).
concurrency_limit429Every line on the account is in use.Retry after Retry-After (5 seconds).
call_state_unknown502We may or may not have placed the call.Check the call before placing it again.
upstream_error502We could not place the call, and it was not placed; or a recording could not be fetched.Retry with the same key.
service_unavailable500, 503Briefly 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.

LimitScopeerror_codeRetry-After
120 read requests a minuteAccountrate_limitedUp to 60 seconds.
120 write requests a minute, counted apart from readsAccountrate_limitedUp to 60 seconds.
Your lines: calls live at the same timeAccountconcurrency_limit5 seconds.
Your AI channels: AI calls live at the same time, a smaller number inside your lines. Calls still ringing count.Accountai_concurrency_limit5 seconds (HTTP 409).
3 calls to one number in 10 minutes, across every API laneAccount and numbernumber_floodUp to 600 seconds.
10 calls to one number in a day (IST), across every API laneAccount and numbernumber_floodUntil midnight IST.
A calling pool number's own daily limit, when POST /ai/calls picks a pool numberCalling pooldaily_capUntil midnight IST.
240 requests a minute refused for their keys (401 or 403)Client addressrate_limitedUp to 60 seconds. Only refused requests count, so a working integration never meets it.
20 wrong secrets a minutePublishable key and client addressrate_limitedUp 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.

Calling many numbers
To call a list, pace your requests to your AI channels: start a new call when a terminal event arrives, and treat 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 with service_unavailable and retry_after_sec 30, 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 on call.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_unknown answer 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 a call_state_unknown answer is checked before any new attempt.
  • Your code branches on error_code, waits for Retry-After on 409 and 429, and queues requests that meet calling_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:// verifies X-Agentive-Signature-V2 with a five minute window, de-duplicates on the event id, and answers 2xx quickly.
  • You match results on data.unique_id or your own metadata, and read call.analysed for 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