Voice Broadcast API

Voice Broadcast API

Place recorded and Interactive Voice calls, trigger saved campaigns, and read call results from your own systems over a JSON REST API.

Base URL
https://voice.agentive.co.in/api/public/v1
Format
JSON over HTTPS
Auth
x-api-key + x-api-secret

Introduction

The Voice Broadcast API gives your application programmatic access to the same calling stack that powers the dashboard. Place a single recorded or Interactive Voice call, run a saved campaign for one number, poll the result of any call you started, page through your call history, and download recordings.

Every amount is in Indian Rupees (INR). Every timestamp is in Indian Standard Time (IST), in ISO 8601 format. Every request is authenticated with a publishable key and a secret, and every response uses one consistent JSON envelope so your integration code stays simple.

When to use it

  • Send an order confirmation, a delivery alert or a payment reminder as a recorded voice call straight from your backend.
  • Run an Interactive Voice call (press 1) and act on the key the recipient presses, or bridge them to your team.
  • Trigger a campaign configured in the dashboard for one number at a time, from a CRM, a form submit or a workflow tool.
  • Reconcile results: poll a call, list calls in bulk, download the recording, and check your balance before a large run.
Billing
All calls are billed in INR against your account balance. Voice broadcast, Interactive Voice and verification calls bill at your account's rate, with a one-minute minimum per answered call and per-second billing after the first minute. Interactive Voice calls that accept spoken responses carry a small per-minute surcharge. Check your balance any time with Get account balance.
One base URL, four references
This page covers calls, campaigns, call history, recordings and balance. Verification codes have their own reference, the Voice OTP API, AI voice agent calls have the AI Voice Agent API, and real-time call events are delivered by Telephony Webhooks instead of polling. All four share the base URL, credentials, envelope, error codes and rate limits described here.
API access is enabled per account
If you get a 403 saying the API is not enabled, contact support. Each endpoint also requires its product to be enabled on your account: the call and campaign endpoints need the voice broadcast features, recording download needs call recording, and account balance needs the wallet feature. A request against a product that is not enabled returns HTTP 403 with "This feature is not enabled for your account."

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.

Base URL

Every endpoint path on this page is relative to this base URL. The Voice OTP API uses the same base.

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

Authentication

Every request must be authenticated with two credentials issued to your account.

CredentialWhat it isHow to send it
Publishable keyNames your account on every request. Starts with pk_live_. Despite the name, treat it as confidential and keep it with the secret.x-api-key request header
Secret keyYour account's private proof. Starts with sk_live_. Treat it like a password.x-api-secret request header (preferred), or a secret field in the JSON body

View and rotate both keys from the API tab in your Voice dashboard. The secret is shown in full exactly once, when it is first generated or rotated. It is stored hashed on our side, so it can never be shown again, by you or by us. If you lose it, rotate to generate a new pair, and the old secret stops working immediately.

The header form is recommended because it keeps the secret out of request bodies and most log formats. If you send the secret in both the header and the body, the header value is used. There is a single set of live credentials per account; both keys begin with a _live_ prefix and every call is processed against your live account.

Example: authenticated request

cURL
curl https://voice.agentive.co.in/api/public/v1/calls \
  -H "x-api-key: pk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "x-api-secret: sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "type": "audio_blast", "number": "9812345678", "campaign_id": 4821 }'

HTTP Basic authentication

As an alternative to the two headers, you may send the credentials with HTTP Basic auth, using the publishable key as the username and the secret as the password. This is the familiar curl -u form. When an x-api-key header is present it takes precedence; otherwise the Basic credentials are used.

cURL
curl https://voice.agentive.co.in/api/public/v1/account \
  -u pk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Keep the secret server-side
Never embed the secret key in a browser, mobile app, or any client a customer can inspect. All requests must be over HTTPS. If a key is exposed, rotate it from the dashboard at once.

Authentication failures

StatusCodeerror_codeWhen
401401missing_credentialsNo publishable key sent.details: "Missing API key. Send your publishable key in the x-api-key header."
401401invalid_credentialsUnknown publishable key.details: "Invalid API credentials."
401401invalid_credentialsKnown key, wrong or missing secret.details: "Invalid API credentials."
403403account_inactiveAccount is not active.details: "This account is not active. Please contact support."
403403api_disabledAPI access not enabled for the account.details: "The API is not enabled for this account. Please contact support."
403403feature_disabledThe endpoint's product is not enabled for the account.details: "This feature is not enabled for your account."

The wording and the status are intentionally the same for an unknown key and a wrong secret, so a caller cannot learn whether a key exists by watching which code comes back. 401 always means the credentials were rejected; 403 always means the credentials were accepted and the account or the product is not entitled.

Changed in September 2026
A known publishable key sent with a wrong secret used to return HTTP 403. It now returns 401, with the same "Invalid API credentials." sentence. If your client branches on 403 to mean "bad secret", move that branch to 401. Nothing else about the response changed.

Response format

Every response is JSON and uses one consistent envelope.

JSON
{
  "status": "success",
  "code": 200,
  "unique_id": "run_4821",
  "reference_id": "order-99213",
  "details": "Call queued."
}

Envelope fields

FieldTypeDescription
statusstring
"success" or "error".
codenumber
A numeric result code. Mirrors the HTTP status (see Error codes).
unique_idstring | null
Our identifier for the resource created by this call. Use it with Get call status.
reference_idstring | null
Echoes the reference_id you sent, so you can correlate the response with your own records. null if you did not send one.
detailsstring
A short human-readable description of the result.
error_codestring
Error responses only. A stable machine string naming the exact condition. Branch on this rather than on details. See Error codes.
errorstring
Error responses only. The same sentence as details, under the key the WhatsApp Business API uses, so one handler can read both products.

Successful responses may include extra fields alongside the envelope. These are documented per endpoint (for example call_status and duration_sec on Get call status, and data plus pagination on the list endpoints).

HTTP status
A successful request returns HTTP 200 with status: "success". A failed request returns the matching HTTP error status (400, 401, 402, 403, 404, 409, 422, 429, or 5xx) with status: "error", and the same value in the code field.

Error codes

Every error carries three things: the HTTP status, the numeric code that mirrors it, and a string error_code naming the exact condition. Branch on error_code. The numeric code is kept for compatibility and the sentence in details (repeated in error) is for your logs, never for a customer.

HTTPMeaningWhen it happens
200SuccessThe request was accepted.
400Bad requestA required field is missing or invalid (a malformed number, an unknown type, a missing campaign_id, a campaign with no recording, or a supplied code that is not 4 to 8 digits).
401UnauthorizedThe credentials were rejected: no key, an unknown key, or a wrong secret. On POST /otp/verify it also reports an incorrect code.
402Balance too lowYour balance cannot cover the call. Nothing was created. Top up and retry.
403ForbiddenThe credentials were accepted but the account is not active, API access is not enabled, the endpoint's product is not enabled, or a trial may not call this number.
404Not foundThe campaign, call or verification does not exist for your account, or the call has no recording. A foreign id is a 404, never a 403.
409ConflictOutside calling hours, or a request with the same Idempotency-Key is still in flight, or the account's AI channels are all busy. Every 409 carries Retry-After.
422Idempotency key reuseThe same Idempotency-Key was sent with a different request body. A new request needs a new key.
429Too many requestsA limit was hit: your request budget, a per-number cap, a daily cap, or your account's lines. Every 429 carries Retry-After.
502Upstream problemWe could not place the call. If we do not know whether it went out, error_code is call_state_unknown.
503Temporarily unavailableBriefly unavailable on our side. Safe to retry after Retry-After.

error_code values

A closed set. New values are added only with a note in the changelog, so a client may treat an unknown one as a generic failure of its HTTP class.

error_codeHTTPMeans
missing_credentials401No publishable key was sent.
invalid_credentials401The key is unknown, or the secret does not match it.
account_inactive403The account is not active.
api_disabled403API access is not enabled for the account.
feature_disabled403The product this endpoint belongs to is not enabled.
trial_restricted403A trial account may only call its registered number.
validation_error400, 422A field is missing, unknown or unusable.
invalid_number400The destination is not a valid Indian mobile number.
invalid_caller_id400caller_id is not yours, or is not enabled for voice broadcast.
invalid_code400A supplied verification code is not 4 to 8 digits.
calling_hours409Outside the 9:00 AM to 9:00 PM IST window.
idempotency_in_progress409The first request with this key is still running.
idempotency_conflict422This key was used with a different body.
rate_limited429Your per-minute request budget is spent.
number_flood429Too many calls to one destination number.
daily_cap429POST /ai/calls only (AI Voice Agent API): the calling pool numbers you call from reached their daily limit. Placing a call here never answers it.
concurrency_limit429Your account has no free line.
ai_concurrency_limit409Your AI call channels are all busy.
whatsapp_locked403The campaign sends a WhatsApp message and WhatsApp is not on your plan.
whatsapp_not_connected409No WhatsApp account is connected to this account yet.
whatsapp_template_required400The campaign has no WhatsApp account and approved template saved on it.
number_locked429Too many wrong codes for this number; send and verify are locked for it.
insufficient_balance402Your balance cannot cover the call.
not_found404No such resource on your account.
conflict409A state conflict with no more specific name. Read details and Retry-After.
upstream_error502We could not place the call, and it was not placed.
call_state_unknown502We asked the network to place the call and lost track of it.
service_unavailable503Briefly unavailable on our side, or your calling-hours setting could not be read (retry_after_sec 30); nothing was placed.

Five more appear only on POST /otp/verify: incorrect_code, max_attempts, code_expired, code_used and verification_not_found.

The split between 429 and 5xx matters for retry logic. A 429 means a limit was hit, so wait for Retry-After. A 502 or 503 means a problem on our side, so a retry after a short pause is right, with one exception: call_state_unknown means we do not know whether the call went out, so poll before you retry. Error responses never include internal infrastructure detail.

Example error response

JSON
{
  "status": "error",
  "code": 400,
  "unique_id": null,
  "reference_id": "order-99213",
  "details": "A valid Indian mobile number is required.",
  "error_code": "invalid_number",
  "error": "A valid Indian mobile number is required."
}

Rate limits

Six limits protect the shared calling network. Each one refuses with HTTP 429, names itself in error_code, and says how long to wait in both the Retry-After header and the body's retry_after_sec.

LimitWindowScopeerror_codeRetry-After
240 failed requests1 minuteClient addressrate_limitedUp to 60 seconds. Only requests refused for their credentials or account (401 or 403) count, so a working integration never meets it.
20 failed credential checks1 minutePublishable key and client addressrate_limitedUp to 60 seconds. Guards a key against someone guessing its secret. A request with the correct secret is always let through, so nobody can lock your integration out.
120 read requests1 minuteAccountrate_limitedUp to 60 seconds.
120 write requests1 minuteAccountrate_limitedUp to 60 seconds. Counted separately from reads, so a polling loop can never starve call placement.
3 calls to one number10 minutesAccount and destination numbernumber_floodUp to 600 seconds.
Your linesWhile calls are liveAccountconcurrency_limit5 seconds.

The RateLimit-* headers on a response describe the per-account bucket for that request's method: a GET reports the read bucket, a POST the write bucket. The X-RateLimit-* headers carry the same three values under the older names, so use whichever your client already reads.

Headers on every response
RateLimit-Limit: 120
RateLimit-Remaining: 117
RateLimit-Reset: 41
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
X-RateLimit-Reset: 41

When you exceed a request limit you receive HTTP 429 with:

JSON
{
  "status": "error",
  "code": 429,
  "unique_id": null,
  "reference_id": null,
  "details": "Too many requests. Please slow down and retry shortly.",
  "error": "Too many requests. Please slow down and retry shortly.",
  "error_code": "rate_limited",
  "retry_after_sec": 60
}

Your lines

A line is one call your account can have live at the same time. It is the number you bought, and everything on the account draws from it: inbound calls, calls your team makes, campaign dials, AI calls and every call you place through this API. Read it any time from Get account balance, which returns lines and lines_in_use. Responses to call-placing requests (POST /calls, POST /campaigns/:id/trigger and POST /otp/send) also carry X-Agentive-Lines and X-Agentive-Lines-In-Use, whether the call was admitted or refused, so a run can pace itself without an extra request. The read endpoints do not send them.

A call your account has no free line for is refused, never queued: HTTP 429 with error_code: "concurrency_limit", lines, lines_in_use and a Retry-After of 5 seconds. That way you always know what happened, instead of waiting on a call that was silently parked.

Headers on a call-placing response
X-Agentive-Lines: 10
X-Agentive-Lines-In-Use: 2
HTTP
HTTP/1.1 429 Too Many Requests
Retry-After: 5
X-Agentive-Lines: 10
X-Agentive-Lines-In-Use: 10

{
  "status": "error",
  "code": 429,
  "unique_id": null,
  "reference_id": "order-99213",
  "details": "All 10 lines on this account are in use. Retry in a few seconds.",
  "error": "All 10 lines on this account are in use. Retry in a few seconds.",
  "error_code": "concurrency_limit",
  "lines": 10,
  "lines_in_use": 10,
  "retry_after_sec": 5
}
What queued means
A call reported as queued has been accepted and is waiting only for the network to place it. It is never waiting for a free line, because a request with no free line is refused before anything is created. So queued always means the call is on its way.

AI voice agent calls carry a second, narrower cap inside your lines, reported with HTTP 409 and error_code: "ai_concurrency_limit". See the AI Voice Agent API.

Per-number call cap
All calls placed through the API to the same destination number (broadcast, Interactive Voice and verification calls alike) are capped at 3 calls per 10 minutes per account. Exceeding it returns HTTP 429 with error_code: "number_flood". A 429 always means wait; only 502 and 503 are safe to retry straight away.
Verification calls carry more limits
POST /calls with type: "otp" is the same request as Send a code and is held to the verification limits as well: at least 30 seconds between two sends to one number, at most 10 in 24 hours, and a number lock after 10 wrong codes in an hour. They are listed in full on the Voice OTP page.

Endpoints

The write endpoints place a call and trigger a saved campaign. The read endpoints fetch a single call's status, list your calls, download a recording, list your campaigns, and report your account balance.

EndpointPurpose
POST /callsPlace a recorded, Interactive Voice or verification call to one number.
POST /campaigns/:id/triggerRun a saved campaign for one number.
GET /calls/:unique_idPoll the status of a call you placed.
GET /callsPage through your call history with filters.
GET /calls/:unique_id/recordingDownload a call recording.
GET /campaignsList the campaigns you can trigger.
GET /accountRead your wallet balance in INR.

Place a call

POST/calls

Place a single outbound call immediately. The type field selects what happens on the call: audio_blast plays a clip from your audio library and ends, press1 plays a clip and then acts on the key the recipient presses, and otp reads a verification code out to the recipient. The response is the standard envelope with unique_id set to the queued call's id.

Headers

HeaderTypeRequiredDescription
x-api-keystringRequired
Your publishable key (pk_live_...). See Authentication.
x-api-secretstringRequired
Your secret key (sk_live_...). Keep it server-side.
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 request so a network retry can never place a second billed call. See Idempotency.
Example: order-99213-call-1

Request body

FieldTypeRequiredDescription
typestringRequired
One of audio_blast, press1, otp. See Call types.
Example: audio_blast
numberstringRequired
The recipient's Indian mobile number. See Phone number format.
Example: 9812345678
campaign_idnumberRequired
Which campaign this call is for. Required for audio_blast and press1, and it must be a campaign of that same type on your account. The campaign holds the recording, the caller number, what each keypress does and every other setting; your request supplies the number to call. Optional for otp, where omitting it uses your account's authentication campaign.
Example: 412
codestringOptional
otp only. Supply your own code: 4 to 8 digits, nothing else. Anything shorter, longer or non-numeric is refused with HTTP 400 and invalid_code. If you omit it, a code is generated for you.
lengthnumberOptional
otp only. Length of the generated code. Default 4; send 6 for anything that guards money or an account. Values outside 4 to 8 are clamped into range, not refused.
Example: 6
ttlnumberOptional
otp only. Seconds the code stays valid. Default 600 (10 minutes). Values outside 60 to 1800 are clamped into range.
max_attemptsnumberOptional
otp only. Verify attempts allowed for this code. Default 3. Values outside 1 to 10 are clamped into range.
messagestringOptional
otp only. A custom spoken message. Use {code} where the code should be read out.
reference_idstringOptional
Your own correlation key. Echoed back and included in webhooks. See Reference IDs.
Example: order-99213

Request

curl https://voice.agentive.co.in/api/public/v1/calls \
  -H "x-api-key: pk_live_..." \
  -H "x-api-secret: sk_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-99213-call-1" \
  -d '{
    "type": "audio_blast",
    "number": "9812345678",
    "campaign_id": 4821,
    "reference_id": "order-99213"
  }'

Response (audio_blast and press1)

JSON
{
  "status": "success",
  "code": 200,
  "unique_id": "run_4821",
  "reference_id": "order-99213",
  "details": "Call queued."
}

Errors

StatusCodeerror_codeWhenWhat to do
400400invalid_numbernumber is missing, malformed, or not an Indian mobile number.details: "A valid Indian mobile number is required."Send a 10-digit mobile number beginning with 6, 7, 8 or 9. See Phone number format.
400400validation_errortype is unknown, campaign_id is missing on a audio_blast or press1 call, the campaign has no recording yet, or the request tries to set something the campaign owns.Use one of the three call types and name a campaign of that type. Change the recording, the caller number or what a keypress does on the campaign itself.
404404not_foundThe campaign_id is not on your account, or the campaign has been deleted. Deleting a campaign stops every call for it, on this endpoint and on the trigger endpoint alike.details: "Campaign not found."Check the id in your dashboard. If you deleted the campaign, create it again and use the new id.
400400validation_errorThe campaign is switched off. Pausing or stopping a campaign stops every call for it. A campaign sitting in draft or completed is normal for API use and keeps working.details: "That campaign is paused. Start it again in your dashboard before placing calls for it." or the same wording for a stopped campaign.Start the campaign again in your dashboard, then resend.
400400validation_errorThe campaign's recording has been deleted since it was set up. We refuse rather than accept a call that would go out silent.details: "The recording this campaign plays is no longer in your audio library."Add a recording to the campaign, then place the call.
400400invalid_caller_idWhat the campaign picked has stopped being usable since it was set up. We refuse rather than call from some other number without telling you.details: "The number this campaign calls from is no longer available for voice broadcast on your account." or "The rotational pool this campaign uses is not available on your account any more."Open the campaign, pick a caller number or a rotational pool again, and resend.
400400invalid_codetype: "otp" with a code that is not 4 to 8 digits.Send 4 to 8 digits, or omit code and let one be generated.
401401invalid_credentialsThe publishable key is unknown, or the secret does not match it.details: "Invalid API credentials."Check both headers. Before September 2026 a known key with a wrong secret returned 403; it now returns 401 like every other credential failure.
402402insufficient_balanceYour balance is below the minimum needed to place a call. Checked before anything is created, so nothing was queued and nothing was charged. The body carries balance_inr and minimum_balance_inr. From September 2026 that minimum is this call's own per-call rate on your account rather than a flat figure, so the pre-flight refuses only what the dialler itself would have refused a moment later.Top up and retry with the same Idempotency-Key.
403403feature_disabledVoice broadcast is not enabled for your account.details: "This feature is not enabled for your account."Contact support to enable it.
403403trial_restrictedThe account is on a trial and the number is not its registered mobile.Complete business verification, or call the registered number.
409409calling_hoursThe request arrived outside calling hours. Verification calls are exempt.Queue the call and retry from 9:00 AM IST. Retry-After says how long that is.
409409idempotency_in_progressA request with the same Idempotency-Key is still being processed.Wait for Retry-After (2 seconds) and retry with the same key.
422422idempotency_conflictThe same Idempotency-Key was sent with a different request body.Use a fresh key for a new request.
429429rate_limitedYou went past your account's per-minute request budget.Retry-After is up to 60 seconds. Pace with the RateLimit headers.
429429number_floodMore than 3 calls to this destination number in 10 minutes.Retry-After is up to 600 seconds.
429429concurrency_limitYour account has no free line for this call. The body carries lines and lines_in_use.Retry-After is 5 seconds. Watch X-Agentive-Lines-In-Use to pace a run.
502502upstream_errorThe calling network refused the request outright.details: "Could not place the call right now. Please retry."Retry after a short pause with the same Idempotency-Key. If that reply comes back as call_state_unknown, stop and follow that row instead: a 502 is never proof on its own that nothing was dialled.
502502call_state_unknownWe asked the network to place the call and then lost track of it, so we cannot say whether it went out. The first reply carries the sentence quoted here; replaying the same Idempotency-Key returns it with a second sentence appended: "The call may or may not have been placed. Check the call with GET /calls/{unique_id} before retrying, or retry with a new Idempotency-Key."details: "Could not place the call right now. Please retry."Do not blind-retry. Poll Get call status with the unique_id, or choose a fresh key deliberately.
503503service_unavailableThe service is briefly unavailable, 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.

Call types

typeWhat it doesRequired fields
audio_blastPlays the campaign's recording to the number, then ends.number, campaign_id
press1Places an Interactive Voice call: plays the campaign's recording, then acts on the key the recipient presses (connect them onward, capture their response, record an opt-out, or send an approved WhatsApp template). Everything it does, including spoken yes/no responses and guided flow steps, is whatever the campaign is set up to do.number, campaign_id
otpPlaces a verification call that reads out a code. A shortcut to the same call the Voice OTP API sends.number

Interactive Voice (press1) example body

JSON
{
  "type": "press1",
  "number": "9812345678",
  "campaign_id": 4822,
  "reference_id": "promo-march"
}
The campaign is the configuration
A broadcast or Interactive Voice call is always a campaign's call. The recording, the caller number, what each keypress does and every other setting live on the campaign you name in campaign_id; your request supplies the number to call. That is also why those calls show up under the campaign in your dashboard, alongside everything else it has sent.

For the otp type the response carries request_id and expires_in_sec and is identical to Send a code in the Voice OTP API, which also documents how to verify the code the recipient enters.

Retries are safe with an Idempotency-Key
This endpoint accepts an Idempotency-Key header. Send one on every request so a network retry can never place a second billed call. See Idempotency.

Trigger a saved campaign

POST/campaigns/:id/trigger

Run an existing API campaign for a single number. Identical in effect to POST /calls with a campaign_id; use whichever shape suits your client. The trigger uses the campaign exactly as configured in your dashboard, including its audio clips, response mode (keypad, keypad plus spoken yes/no, or spoken only), guided flow steps, voicemail detection, ring timeout, custom response labels, key map and wait times, and caller number. Audio Blast, Interactive Voice and AI voice agent campaigns can be triggered this way; the response is the usual run_... id. A campaign whose response action is WhatsApp sends its template after the call; the trigger is refused when WhatsApp is locked or not connected on the account (see the errors below). Verification-code campaigns are sent with the Voice OTP API instead.

Headers

HeaderTypeRequiredDescription
x-api-keystringRequired
Your publishable key (pk_live_...). See Authentication.
x-api-secretstringRequired
Your secret key (sk_live_...). Keep it server-side.
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 request so a network retry can never place a second billed call. See Idempotency.
Example: order-99213-call-1

Path parameters

ParameterTypeRequiredDescription
idnumberRequired
The numeric id of the campaign to trigger. Find it with List campaigns.
Example: 57

Request body

FieldTypeRequiredDescription
numberstringRequired
The recipient's Indian mobile number. See Phone number format.
Example: 9812345678
reference_idstringOptional
Your own correlation key. Echoed back and included in webhooks.
Example: lead-5567

Request

curl https://voice.agentive.co.in/api/public/v1/campaigns/57/trigger \
  -H "x-api-key: pk_live_..." \
  -H "x-api-secret: sk_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: lead-5567-call-1" \
  -d '{ "number": "9812345678", "reference_id": "lead-5567" }'

Response

JSON
{
  "status": "success",
  "code": 200,
  "unique_id": "run_4822",
  "reference_id": "lead-5567",
  "details": "Call queued."
}

Errors

StatusCodeerror_codeWhenWhat to do
404404not_foundThe campaign id does not exist on your account, or is not an API campaign.details: "Campaign not found."Pick an id from List campaigns.
400400validation_errorThe campaign is a verification-code campaign.details: "This is a verification-code campaign. Use the OTP endpoints to send a code."Send the code with the Voice OTP API.
400400validation_errorThe campaign type cannot be triggered for one number.details: "This campaign type cannot be triggered for a single number."Use an Audio Blast or Interactive Voice campaign.
400400validation_errorThe request sent variables to an Audio Blast or Interactive Voice campaign. Only an AI voice agent campaign takes them.details: "This campaign type does not take variables."Leave variables out of the request.
400400validation_errorThe request sent metadata to an Audio Blast or Interactive Voice campaign. Only an AI voice agent campaign takes it.details: "This campaign type does not take metadata."Leave metadata out of the request. Use reference_id to match the call to your records.
400400validation_errorThe campaign has no audio clip configured.details: "This campaign has no audio configured."Attach an audio clip to the campaign in the dashboard.
400400validation_errorAn AI voice agent campaign whose agent has been removed or paused. See the AI Voice Agent API.details: "This campaign has no active agent configured."Reattach an active agent to the campaign in the dashboard.
409409ai_concurrency_limitAn AI voice agent campaign when the account's AI channels are all busy. This is a second, narrower cap inside your account's lines.details: "All your AI call channels are in use. Retry when a call finishes."Retry-After is 5 seconds. Placing a non-AI call is unaffected.
403403whatsapp_lockedThe campaign sends a WhatsApp message after the call, and WhatsApp is not part of this account's plan.details: "WhatsApp is locked on this account. Add WhatsApp to your plan, upgrade to a yearly phone plan, or move to Phone + CRM."Add WhatsApp to the plan, upgrade to a yearly phone plan, or move to Phone + CRM.
409409whatsapp_not_connectedThe plan allows WhatsApp, but no WhatsApp account is connected yet.details: "Connect Agentive Chat on the Integrations page before using WhatsApp in a campaign."Connect Agentive Chat on the Integrations page.
400400whatsapp_template_requiredThe campaign uses the WhatsApp action but has no account and approved template saved on it.details: "Pick the WhatsApp account and an approved template for this campaign first."Pick an account and an approved template on the campaign in the dashboard.

Balance (402), calling-hours and idempotency conflicts (409, 422), rate limits and the line refusal (429) behave exactly as on Place a call. Like every billed POST endpoint, the trigger accepts an Idempotency-Key header (see Idempotency).

An AI voice agent campaign is triggered the same way and also takes variables and metadata; it is documented in the AI Voice Agent API. A recorded-message or Interactive Voice campaign refuses both: 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."

Get call status

GET/calls/:unique_id

Check the current state of a call you placed. Pass the unique_id from the placement response. A raw call_id from List calls and a verification request_id both resolve here too, each with its own response shape, all three described below.

Path parameters

ParameterTypeRequiredDescription
unique_idstringRequired
The unique_id returned when you placed the call, for example run_4821. Raw call ids also resolve here: the call_id values from List calls. Must belong to your account.
Example: run_4821

Request

curl https://voice.agentive.co.in/api/public/v1/calls/run_4821 \
  -H "x-api-key: pk_live_..." \
  -H "x-api-secret: sk_live_..."

Response (a run id, for example run_4821)

JSON
{
  "status": "success",
  "code": 200,
  "unique_id": "run_4821",
  "reference_id": "order-99213",
  "details": "answered",
  "call_status": "answered",
  "duration_sec": 17,
  "call_id": "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
  "end_reason": null,
  "call": {
    "unique_id": "run_4821",
    "call_id": "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
    "leg_id": null,
    "direction": "outbound",
    "from_number": "01100000021",
    "to_number": "+919812345678",
    "status": "in_progress",
    "end_reason": null,
    "duration_sec": 17,
    "campaign_id": 4821,
    "reference_id": "order-99213",
    "created_at": "2026-06-04T21:42:08+05:30",
    "created_at_ist": "2026-06-04T21:42:08+05:30",
    "created_at_utc": "2026-06-04T16:12:08Z"
  }
}

Response fields

FieldTypeDescription
call_statusstring
The call's current state in the older, smaller vocabulary: queued, ringing, answered, completed or failed. Unchanged. The mapping to the full set is in Call status and end reasons.
duration_secnumber | null
Talk duration in seconds, once the call has been answered.
call_idstring | null
Our record id for the call this run placed. Use it for that leg's recording.
end_reasonstring | null
Why the call ended, from the enumerated end reasons. null while the call is still running. Read this to tell a call_status of failed that rang unanswered from one that never connected.
reference_idstring | null
The reference_id you sent when you placed the call.
callobject | null
The full call object with the enumerated status and end_reason values, once the call exists.

Errors

StatusCodeerror_codeWhenWhat to do
404404not_foundThe unique_id does not exist or does not belong to your account.details: "Call not found." or "Not found."Check the id you stored from the placement response.

Response (a raw call id)

When the path is a raw call id (a call_id from List calls), the call object carries four extra fields beside the standard call object.

JSON
{
  "status": "success",
  "code": 200,
  "unique_id": "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
  "reference_id": "order-99213",
  "details": "completed",
  "call_status": "completed",
  "duration_sec": 42,
  "call": {
    "unique_id": "run_4821",
    "call_id": "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
    "leg_id": null,
    "direction": "outbound",
    "from_number": "01100000021",
    "to_number": "+919812345678",
    "status": "completed",
    "end_reason": "completed",
    "duration_sec": 42,
    "campaign_id": 4821,
    "reference_id": "order-99213",
    "created_at": "2026-06-04T21:42:08+05:30",
    "created_at_ist": "2026-06-04T21:42:08+05:30",
    "created_at_utc": "2026-06-04T16:12:08Z",
    "agent_id": null,
    "transcript_available": false,
    "analysis_available": false
  }
}

Extra fields on call

FieldTypeDescription
duration_secnumber | null
Talk duration in seconds. Also returned beside the envelope.
agent_idnumber | null
The AI voice agent that handled the call. null for a recorded or Interactive Voice call, and for any call a person handled.
transcript_availableboolean
Whether a transcript of this call has been produced.
analysis_availableboolean
Whether an analysis of this call has been produced.

A verification request id from the Voice OTP API resolves here too. For those the response carries verification_status (the closed set pending, verified, failed, locked, expired, undeliverable), the original call_status, plus verified, attempts, max_attempts and expires_at_ist. The Voice OTP reference documents that shape.

Listing calls in bulk
This endpoint reports a single call you started. To page through your call history (including inbound calls), use List calls, which returns the richer call object with the enumerated status and end_reason values.

List calls

GET/calls

Page through the calls on your account, most recent first. This returns the full call object for each call, with the enumerated status and end_reason values, so you can reconcile call results in bulk instead of polling each call by id.

Query parameters

ParameterTypeRequiredDescription
pagenumberOptional
The page of results to return. Default 1.
per_pagenumberOptional
Results per page. Default 50, maximum 100.
directionstringOptional
Filter by inbound or outbound.
statusstringOptional
Filter by a call status: queued, ringing, in_progress, completed, failed, or missed.
fromstringOptional
Include calls created on or after this ISO 8601 date or timestamp.
Example: 2026-06-01
tostringOptional
Include calls created before this ISO 8601 date or timestamp.
Example: 2026-06-30T23:59:59+05:30
numberstringOptional
Match calls where this number appears as either the caller or the recipient.
Example: 9812345678

Request

curl "https://voice.agentive.co.in/api/public/v1/calls?per_page=50&direction=outbound&status=completed" \
  -H "x-api-key: pk_live_..." \
  -H "x-api-secret: sk_live_..."

Response

JSON
{
  "status": "success",
  "code": 200,
  "data": [
    {
      "unique_id": "run_4821",
      "call_id": "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
      "leg_id": null,
      "direction": "outbound",
      "from_number": "01100000021",
      "to_number": "+919812345678",
      "status": "completed",
      "end_reason": "completed",
      "duration_sec": 42,
      "campaign_id": 4821,
      "reference_id": "order-99213",
      "created_at": "2026-06-04T21:42:08+05:30",
      "created_at_ist": "2026-06-04T21:42:08+05:30",
      "created_at_utc": "2026-06-04T16:12:08Z"
    }
  ],
  "pagination": {
    "page": 1,
    "per_page": 50,
    "total": 137,
    "total_pages": 3
  }
}

Response fields

FieldTypeDescription
dataobject[]
A page of call objects.
pagination.pagenumber
The current page.
pagination.per_pagenumber
Results per page for this response.
pagination.totalnumber
Total calls matching your filters.
pagination.total_pagesnumber
Total number of pages.

The response carries only the public call object fields. end_reason is always one of the enumerated end reasons.

Download a call recording

GET/calls/:unique_id/recording

Download the audio recording of a call. The API streams the file itself: HTTP 200 with the audio as the body and a Content-Type of audio/ogg or audio/wav, depending on how the recording was stored. It never redirects you to another address. Send a Range header to fetch part of the file (HTTP 206); a range outside the file answers HTTP 416. The samples authenticate with HTTP Basic, which an HTTP client never forwards to another host, and you should never send your secret anywhere but this API. Requires call recording to be enabled on your account.

Path parameters

ParameterTypeRequiredDescription
unique_idstringRequired
The unique_id returned when you placed the call, for example run_4821. Raw call ids also resolve here: the call_id values from List calls. Must belong to your account.
Example: run_4821

Request

# The API streams the audio. Basic auth: key as user, secret as password.
curl "https://voice.agentive.co.in/api/public/v1/calls/run_4821/recording" \
  -u "$AGENTIVE_KEY:$AGENTIVE_SECRET" \
  --output call-4821.ogg

Response (no recording available)

JSON
{
  "status": "error",
  "code": 404,
  "unique_id": null,
  "reference_id": null,
  "details": "No recording is available for this call.",
  "error": "No recording is available for this call.",
  "error_code": "not_found"
}

Errors

StatusCodeerror_codeWhenWhat to do
404404not_foundThe call never connected, or the audio is not available.details: "No recording is available for this call."Wait for the recording.available event, or check that the call reached completed.
403403feature_disabledCall recording is not enabled for your account.details: "This feature is not enabled for your account."Contact support to enable call recording.
Get notified when it is ready
A recording is typically ready a short time after the call ends. To be notified the moment it is ready, subscribe to the recording.available webhook, which carries this endpoint's address as recording_url.

List campaigns

GET/campaigns

List the campaigns on your account that can be triggered through the API. Use a campaign id from this list with Trigger a saved campaign.

Request

curl https://voice.agentive.co.in/api/public/v1/campaigns \
  -H "x-api-key: pk_live_..." \
  -H "x-api-secret: sk_live_..."

Response

JSON
{
  "status": "success",
  "code": 200,
  "data": [
    { "id": 57, "name": "March promo", "type": "audio_blast", "status": "active", "press1_action": null },
    { "id": 61, "name": "Renewal interactive voice", "type": "press1", "status": "active", "press1_action": "whatsapp" },
    { "id": 72, "name": "Signup verification", "type": "otp", "status": "active", "press1_action": null }
  ]
}

Response fields

FieldTypeDescription
idnumber
The campaign id. Use it with Trigger a saved campaign.
namestring
The name you gave the campaign.
typestring
One of audio_blast (Audio Blast), press1 (Interactive Voice), otp (verification) or ai_agent (an AI voice agent takes the conversation). audio_blast and press1 can be triggered from Trigger a saved campaign. otp campaigns are configured here but delivered with the Voice OTP API, and ai_agent campaigns are not available through the API at present.
statusstring
The campaign's current state.
press1_actionstring | null
For press1 campaigns: connect, interest or whatsapp. null for other types.

Get account balance

GET/account

Return your account's current wallet balance in INR. Useful as a pre-flight check before a large run. Requires the wallet feature on your account.

Request

curl https://voice.agentive.co.in/api/public/v1/account \
  -H "x-api-key: pk_live_..." \
  -H "x-api-secret: sk_live_..."

Response

JSON
{
  "status": "success",
  "code": 200,
  "data": {
    "balance_inr": 1842.5,
    "currency": "INR",
    "lines": 10,
    "lines_in_use": 2
  }
}

Response fields

FieldTypeDescription
data.balance_inrnumber
Your current wallet balance, in Indian Rupees.
data.currencystring
Always "INR".
data.linesnumber
The number of calls your account can have live at the same time. Inbound, outbound, campaign and API calls all count against it.
data.lines_in_usenumber
How many of those lines are in use right now, including calls the API has just placed.

Errors

StatusCodeerror_codeWhenWhat to do
403403feature_disabledThe wallet feature is not enabled for your account.details: "This feature is not enabled for your account."Contact support to enable the wallet.

The call object

List calls returns a data array of call objects, and Get call status returns one under call when you fetch by call_id. The same shape appears in the call.* webhook events.

FieldTypeDescription
unique_idstring
The call's public id. For calls placed through an API run this looks like run_4821; otherwise it is the call id.
call_idstring | null
Our internal identifier for the call.
leg_idstring | null
Added September 2026. A second id for the same call, present only when the record is keyed on something other than the phone leg, and null otherwise. Either id resolves on Get call status; unique_id did not change when this field was added.
directionstring
outbound or inbound.
from_numberstring | null
The caller number shown.
to_numberstring | null
The recipient number.
statusstring | null
The call state. One of the enumerated status values.
end_reasonstring | null
Why the call ended. One of the enumerated end reasons, or null while the call is still in progress.
duration_secnumber | null
Talk duration in seconds.
campaign_idnumber | null
The run this call belongs to, matching the digits in unique_id.
reference_idstring | null
The reference_id you supplied.
created_atstring | null
When the call was created. Kept exactly as it has always been returned; prefer the two fields below in new code.
created_at_iststring | null
The same instant in Indian Standard Time, ISO 8601 with a +05:30 offset.
Example: 2026-06-04T21:42:08+05:30
created_at_utcstring | null
The same instant in UTC, ISO 8601 with a Z suffix.
Example: 2026-06-04T16:12:08Z

reference_id is filled on every read for any call we can resolve it for, so a list or a poll gives you back the key you sent. Calls placed before September 2026 may still show null there.

Call 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.

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 recipient sees

The campaign decides, and only the campaign. You pick it once when you set the campaign up, and every call that campaign places comes from what you picked. You cannot set it per request, and nothing overrides it or stands in for it later. It works the same whether you place the call with POST /calls or trigger the campaign directly.

  • If the campaign names one of your numbers, that number is used and the recipient sees your own line. Only your numbers enabled for voice broadcast can be picked.
  • If the campaign names a rotational pool, the call goes out from a number in that pool, and the pool rotates across its numbers and paces them on your behalf.
  • If what the campaign picked stops being usable— the number is switched off for broadcast, quarantined, or the pool is withdrawn — the request is refused with HTTP 400 and invalid_caller_id, telling you to open the campaign and pick again. Your calls are never quietly moved to some other number: you would not know it had happened, and neither would the customer seeing an unfamiliar caller.
  • If your account has no broadcast number and no pool at all, the request is refused with HTTP 400 and "No number on your account is enabled for voice broadcast. Ask your account manager to enable one, then retry." Retrying will not help until a number is enabled.
A team line is never used
A number your team answers calls on is never used as a broadcast caller ID, even if it is the only number on the account, and even if it was set on the campaign before it became a team line. If that is all you have, ask your account manager for a broadcast number or a rotational pool.

Phone number format

Send Indian mobile numbers in any of these forms; they are all normalised:

  • 10-digit local: 9812345678
  • With country code: 919812345678
  • With plus prefix: +919812345678
Mobile numbers only
The mobile number must begin with 6, 7, 8, or 9. Landline and clearly invalid numbers are rejected with HTTP 400 and "A valid Indian mobile number is required."

Reference IDs

reference_id is an optional string you attach to any request, such as an order number, ticket id, or signup id. It is:

  • echoed back in the response reference_id field, and
  • included in every webhook for the call.

Use it to match our events back to your own records without storing our ids. Reference IDs are capped at 120 characters; control characters are stripped. A reference_id never deduplicates anything; use an Idempotency-Key for retry safety.

Calling hours

Calls that reach a customer with a message are placed between 9:00 AM and 9:00 PM IST, every day. This applies to Audio Blast and Interactive Voice calls, whether you place them with POST /calls or by triggering a saved campaign. A request that arrives outside those hours is refused with HTTP 409 and a message naming the hours, so your system can queue it and retry in the morning rather than believing the call was placed. If your account's calling-hours setting cannot be read at that moment, the request is refused too, never placed: HTTP 503 with service_unavailable and retry_after_sec 30. Retry after 30 seconds with the same Idempotency-Key.

Verification codes are the exception
Verification calls (the otp type, and POST /otp/send in the Voice OTP API) work around the clock, because a login or payment code has to reach someone whenever they are signing in.

If your account has been approved for extended hours, your calls are not restricted and you will not see the 409.

Idempotency

Four endpoints accept an Idempotency-Key header: POST /calls, POST /campaigns/:id/trigger, and POST /otp/send and POST /otp/verify in the Voice OTP API. Send a unique key with each logical request, and a network retry can never place a second billed call: if a request times out or the connection drops, resend the same request with the same key.

cURL
curl https://voice.agentive.co.in/api/public/v1/calls \
  -H "x-api-key: pk_live_..." \
  -H "x-api-secret: sk_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-99213-call-1" \
  -d '{ "type": "audio_blast", "number": "9812345678", "campaign_id": 4821, "reference_id": "order-99213" }'

The key is 1 to 128 visible ASCII characters; a longer key is refused with HTTP 400 rather than cut short. Use something unique per action, such as a UUID or your own order id plus an attempt marker. Keys are scoped to your account and live for 24 hours.

What is stored and replayed

A key is consumed only by a response that decided the request. Everything else releases it, so the same key is the right thing to retry with.

ResponseStored?What a retry with the same key does
2xxYesReplays the original response. No second call is placed.
400YesReplays the same validation error for 24 hours, even after you fix the body or the campaign in the dashboard. Send the fixed request with a new key.
401 on verifyYesReplays the incorrect-code response, because the attempt was already spent.
404YesReplays the not-found response for 24 hours. Once the id or the dashboard setting is fixed, send the request with a new key.
422YesReplays the conflict. The key is bound to the first body it saw.
402NoTop up, then resend with the same key. The call is attempted.
403NoOnce the entitlement is in place, resend with the same key.
409NoWait for Retry-After, then resend with the same key.
429NoWait for Retry-After, then resend with the same key.
5xx before we placed the callNoResend with the same key. Nothing was placed.
502 call_state_unknownYesReplays the same answer. Poll GET /calls/:unique_id, or choose a fresh key deliberately.
One case where the same key is not the answer
If a failure happens after we asked the network to place your call, we cannot say whether it went out. That response is stored and replayed as 502 with error_code: "call_state_unknown", so a blind retry can never double-dial. Poll Get call status with the unique_id you were given; only place a second call with a fresh key once you have seen that the first one did not happen.

Replay and in-flight

A replayed response carries two headers with the same meaning: Idempotent-Replayed: true, the platform-wide name, and idempotency-replayed: true, the original one, kept so existing clients keep working.

HTTP
HTTP/1.1 200 OK
Content-Type: application/json
Idempotent-Replayed: true
idempotency-replayed: true

Reusing a key while the first request is still running returns HTTP 409 with error_code: "idempotency_in_progress" and Retry-After: 2, for as long as that first request is in flight. If our side restarts mid-request the key is released at once, so a retry is never stuck behind a request that will never finish.

HTTP
HTTP/1.1 409 Conflict
Retry-After: 2

{
  "status": "error",
  "code": 409,
  "unique_id": null,
  "reference_id": "order-99213",
  "details": "A request with this Idempotency-Key is still being processed. Retry in a moment.",
  "error": "A request with this Idempotency-Key is still being processed. Retry in a moment.",
  "error_code": "idempotency_in_progress",
  "retry_after_sec": 2
}

The same key with a different request body returns HTTP 422 with error_code: "idempotency_conflict". A new request needs a new key.

And a repeat of a key whose first answer was an unknown outcome replays that answer with a second sentence appended, spelling out what to do before you dial again:

JSON
{
  "status": "error",
  "code": 502,
  "unique_id": null,
  "reference_id": "order-99213",
  "details": "Could not place the call right now. Please retry. The call may or may not have been placed. Check the call with GET /calls/{unique_id} before retrying, or retry with a new Idempotency-Key.",
  "error": "Could not place the call right now. Please retry. The call may or may not have been placed. Check the call with GET /calls/{unique_id} before retrying, or retry with a new Idempotency-Key.",
  "error_code": "call_state_unknown"
}

reference_id remains a correlation echo only; it never de-duplicates anything. Use the Idempotency-Key header for retry safety and reference_id to match calls back to your records.

Webhook events are de-dupable
Each webhook event carries a stable id (evt_...) you can use to ignore repeats. See Ordering and idempotency in the webhook reference.

Campaign webhooks

A campaign can push its own results to a URL of yours. You set that URL on the campaign in your dashboard, and it belongs to that campaign alone. It is separate from the account-level Telephony Webhooks, which cover every call on the account and have their own endpoints, retry schedule and delivery log. Campaign webhooks report campaign work: one event when an attempt on a contact is final, and one when a team member sets a disposition.

Campaigns you run from the dashboard fire these events. A campaign you start through Trigger a saved campaign does not: every API trigger runs as its own child of the campaign, and those children do not inherit the campaign's webhook URL. For API-driven calls, use the account-level Telephony Webhooks instead.

The two events

EventWhen it is sent
campaign.attempt_endOne per contact, when the attempt on that contact is final. Never on a retry, so a contact you dial three times produces one event, at the end.
campaign.disposition_setWhen a team member sets a disposition on a call from this campaign.

Headers

HeaderTypeDescription
Content-Typestring
The body is always JSON.
Example: application/json
User-Agentstring
Fixed, and the same string campaign webhooks have always sent, so an allow-list built on it keeps working. Do not use it as proof the event is ours: check X-Agentive-Signature-V2 for that.
Example: agentive-voice-webhook/1.0
X-Agentive-Eventstring
The dotted event name, the same value as type in the body, so you can route without parsing the body first.
Example: campaign.attempt_end
X-Agentive-Event-Idstring
The same value as id in the body. De-duplicate on it.
Example: evt_9f3c1a7b40d25e86c1b4
X-Agentive-Timestampstring
Unix seconds at the moment we sent the event, alongside created in the body. It is stamped once per event, so a retry carries the same value as the first attempt, and it can be a second later than created. Do not treat the two as byte-equal.
Example: 1780589528
X-Agentive-Signaturestring
The older, body-only signature: sha256= followed by the hex HMAC-SHA256 of the raw body, keyed with your account's webhook signing secret. It is still sent for receivers built before version 2, but it cannot prove when a body was sent, so do not accept it on its own. The secret is shown on the campaign page and is shared with your call menu key press webhook, so one account has one secret and one verification routine. Both signature headers are absent only when the account has no signing secret, which happens only on a database fault, so an unsigned delivery is never a normal one. See Verifying the signature.
Example: sha256=4f1d...08e9
X-Agentive-Signature-V2string
t= the same Unix seconds as X-Agentive-Timestamp, a comma, then v2= the hex HMAC-SHA256 of the timestamp, a dot and the raw body (t + "." + body), keyed with the same secret. Version 1 signs the body alone, so it is the same however many times the event is sent; version 2 binds the timestamp into what is signed, which is what makes a replay window mean anything. Verify this one only, reject a t more than five minutes from your clock, and refuse a delivery without it. See Verifying the signature.
Example: t=1780589528,v2=9c41...7e05

campaign.attempt_end

Payload

FieldTypeDescription
idstring
The event id (evt_ and 20 hex characters). Stable for this event. De-duplicate on it. The sample the Test button sends is prefixed evt_test_ instead, so it can never collide with a real event id.
typestring
The dotted event name, campaign.attempt_end. Branch on this.
eventstring
The legacy name, which keeps the value attempt_end so existing handlers keep working.
creatednumber
Unix seconds at the moment we sent the event.
timestamp_iststring
The same instant in Indian Standard Time, ISO 8601 with milliseconds and a +05:30 offset.
Example: 2026-06-04T21:42:08.412+05:30
testboolean
false on a real attempt, true only for the sample the Test button on the campaign page sends.
org_idnumber
Your account id.
campaignobject
id, name, mode (the pacing mode), dial_type and parent_campaign_id. dial_type is the internal campaign type (broadcast_dtmf, broadcast_audio, human_pool, ai_agent or auth_otp), not the type value that List campaigns returns.
contactobject
id, phone, name and email of the contact this attempt dialled.
attemptobject
attempt_no, outcome, amd_result, duration_sec, ended_at, call_uuid, disposition and end_reason. The first five are unchanged from the older unsigned payload. end_reason here is the raw carrier hangup cause recorded on the call row (for example NORMAL_CLEARING or USER_BUSY), not the shorter end reason of the call object, and it is null when no cause was recorded.
pressobject | null
What the recipient chose: digit, action (connect, interest, whatsapp, decline or none) and via (keypad or voice). The whole object is null when nothing was pressed or said, and it is always null on a guided-flow campaign, where the answers are reported in flow_answers instead. action is none when this campaign gives the key no meaning: a recipient can press anything on their handset, and a digit that is not in the campaign's key map does nothing, so it is reported as a keypress that did nothing rather than as the campaign's own action. Handle none as a real value rather than a missing one, and do not create a lead from it.
flow_answersobject | null
For a guided flow, the answer to each question keyed q0, q1 and so on, each yes, no or none. none is a question that ran out of retries with no reply, so handle three values, not two. null when the campaign has no flow.
whatsappobject | null
sent, suppressed and error, present only when a WhatsApp message was actually owed for this attempt. null otherwise, which is the common case even on a WhatsApp campaign: the campaign has no WhatsApp action, or the recipient did not choose it. A send that was owed but had not finished by the time we sent the event is still an object, reported as error "pending".
Compatibility
The top-level event field keeps the value attempt_end. Branch on type for the dotted name.

whatsapp is null for one reason only: no WhatsApp message was owed for this attempt. So do not read whatsapp.sent without checking that whatsapp is present. When it is present and sent is false, error names the reason: frequency_suppressed, not_connected, locked, no_template, bad_number, send_refused, network or pending. It is a closed set: send_refused means the message itself was refused, and the wording of that refusal is not published in the event.

pending is the one value that is not a final outcome. It means the send was still in flight when we sent the event: we wait up to 8 seconds after the attempt ends for the message to settle, and past that we report the attempt rather than hold the event back. The message may still go out afterwards, so read the final outcome for that contact on the campaign page.

JSON
{
  "id": "evt_9f3c1a7b40d25e86c1b4",
  "type": "campaign.attempt_end",
  "event": "attempt_end",
  "created": 1780589528,
  "timestamp_ist": "2026-06-04T21:42:08.412+05:30",
  "test": false,
  "org_id": 42,
  "campaign": {
    "id": 412,
    "name": "Renewal interactive voice",
    "mode": "progressive",
    "dial_type": "broadcast_dtmf",
    "parent_campaign_id": null
  },
  "contact": {
    "id": 90218,
    "phone": "+919812345678",
    "name": "Neha Sharma",
    "email": "neha@example.com"
  },
  "attempt": {
    "attempt_no": 1,
    "outcome": "interested",
    "amd_result": "HUMAN",
    "duration_sec": 24,
    "ended_at": "2026-06-04T16:12:08.412Z",
    "call_uuid": "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
    "disposition": "interested",
    "end_reason": "NORMAL_CLEARING"
  },
  "press": { "digit": "1", "action": "whatsapp", "via": "keypad" },
  "flow_answers": null,
  "whatsapp": { "sent": true, "suppressed": false, "error": null }
}

campaign.disposition_set

Sent when a team member sets a disposition on a call from this campaign. It carries the same envelope fields (id, type, event, created, timestamp_ist, test and org_id), the same campaign and contact objects as campaign.attempt_end (contact is { "id": null } when the attempt has no contact row), a call object with uuid, disposition, disposition_by and notes, and set_at. Its legacy event value stays disposition_set.

JSON
{
  "id": "evt_2d70b915ee4a3c68f0a1",
  "type": "campaign.disposition_set",
  "event": "disposition_set",
  "created": 1780590114,
  "timestamp_ist": "2026-06-04T21:51:54.077+05:30",
  "test": false,
  "org_id": 42,
  "campaign": {
    "id": 412,
    "name": "Renewal interactive voice",
    "mode": "progressive",
    "dial_type": "broadcast_dtmf",
    "parent_campaign_id": null
  },
  "contact": {
    "id": 90218,
    "phone": "+919812345678",
    "name": "Neha Sharma",
    "email": "neha@example.com"
  },
  "call": {
    "uuid": "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
    "disposition": "callback",
    "disposition_by": 41,
    "notes": "Wants a call back on Monday"
  },
  "set_at": "2026-06-04T16:21:54.077Z"
}

Verifying the signature

Verify before you trust an event, using X-Agentive-Signature-V2 only. It carries t=<unix seconds>,v2=<hex>. Split it on the comma, take the value of t, and reject the event if t is more than five minutes from your own clock. Then compute the HMAC-SHA256 of the string t, a dot, and the raw request body, keyed with your account's webhook signing secret, hex-encode it and compare it to v2 with a timing-safe comparison, after checking the two are the same length in bytes. Parse the body only after the comparison passes, and read the raw bytes rather than a re-serialised object: any change to spacing or key order changes the signature.

Refuse a delivery that has no version 2 header, and do not fall back to X-Agentive-Signature. That older header signs the body alone, so anyone who captured one delivery could send the same body again later with a fresh timestamp and it would still check out; version 2 puts the timestamp inside what is signed, so the five minute window holds. Every delivery carries both headers, so a version 2 only receiver misses nothing. Never run the check with an empty secret: the samples refuse to start without one.

import crypto from "crypto";
import express from "express";

const app = express();

// Capture the RAW body: the signature covers the exact bytes we sent.
app.use("/webhooks/agentive-campaign", express.raw({ type: "application/json" }));

const TOLERANCE_SEC = 300; // reject anything older than five minutes

// Compare byte lengths first: timingSafeEqual throws on unequal lengths, and a
// multi-byte character can make two strings of equal length differ in bytes.
function safeEqual(a, b) {
  const x = Buffer.from(String(a), "utf8");
  const y = Buffer.from(String(b), "utf8");
  return x.length === y.length && crypto.timingSafeEqual(x, y);
}

// Version 2: t=<unix seconds>,v2=<hex>. HMAC-SHA256 over t, a dot, then the raw body.
function verifyV2(secret, rawBody, header) {
  if (!secret) return false; // never verify with an empty key
  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");
  return safeEqual(parts.v2, expected);
}

// Version 2 only. A delivery without X-Agentive-Signature-V2 is refused: the
// older X-Agentive-Signature signs the body alone and cannot prove freshness.
function verifySignature(secret, rawBody, headers) {
  return verifyV2(secret, rawBody, headers["x-agentive-signature-v2"]);
}

const SECRET = process.env.AGENTIVE_WEBHOOK_SECRET;
if (!SECRET) throw new Error("AGENTIVE_WEBHOOK_SECRET is not set");

app.post("/webhooks/agentive-campaign", (req, res) => {
  if (!verifySignature(SECRET, req.body, req.headers)) {
    return res.status(401).send("bad signature");
  }

  const evt = JSON.parse(req.body.toString("utf8"));

  // De-duplicate on evt.id, then branch on evt.type.
  if (evt.type === "campaign.attempt_end") {
    console.log(evt.contact.phone, evt.attempt.outcome, evt.whatsapp?.sent);
  } else if (evt.type === "ivr.keypress") {
    // from_number is the caller's number as the carrier presented it:
    // match on its last ten digits.
    console.log(evt.data.from_number.slice(-10), evt.data.key_path, evt.data.key_label);
  }

  res.sendStatus(200);
});

app.listen(3000);

Delivery

We POST once and retry once more after 2 seconds if your endpoint returns 5xx, does not answer within 6 seconds, or cannot be reached at all. A 4xx is not retried. Only a 2xx counts as delivered. There is no delivery log for campaign webhooks; the campaign page shows the last delivery result and a Test button that sends one signed sample with test: true.

So answer with any 2xx as soon as you have stored the event, and do your own work afterwards. Because an event can arrive twice, key your handler on id.

Redirects. We follow at most one. If your address answers 3xx we read Location, resolve it against the address we called, and put the result through the same address check every delivery makes: it must be a public http or https address. When it passes we repeat the same POST to it, with the same headers and the same signed body, so what you receive still verifies. Anything else ends the delivery, and it is not retried: a Location we cannot deliver to, a 3xx that gives us no usable Location, or a second redirect. The campaign page names which of the three it was. Each request gets its own 6 second window, so a followed redirect does not eat the first request's time.

Save the final address rather than one that redirects. A hop costs a round trip on every event, and any change to where it points stops delivery until you update the campaign.

Call menu key press webhook

A call menu can post every key a caller presses to a URL of yours: one event per key press, as the caller moves through the menu. The event is sent off the call path. The caller is already on their way to the department, the message or the WhatsApp template that key leads to, and a slow or failing endpoint never delays the call.

You set the URL on the call menu itself, under Call menu > Advanced > Key press webhook. The same page shows your account's webhook signing secret, which this hook shares with campaign webhooks: one account, one secret, one verification routine. It is not one of the events on the account-level Telephony Webhooks page, and it does not appear in that delivery log.

ivr.keypress

Payload

FieldTypeDescription
idstring
The event id (evt_ and 20 hex characters). Stable for this event. De-duplicate on it. The sample the Test button sends is prefixed evt_test_ instead, so it can never collide with a real event id.
typestring
The dotted event name, ivr.keypress. Branch on this.
eventstring
The same value, ivr.keypress, kept alongside type for a handler that reads event.
creatednumber
Unix seconds at the moment we sent the event, the same value as X-Agentive-Timestamp.
timestamp_iststring
The same instant in Indian Standard Time, ISO 8601 with milliseconds and a +05:30 offset.
Example: 2026-06-04T22:12:22.518+05:30
testboolean
false on a real key press, true only for the sample the Test button on the call menu page sends.
dataobject
The key press. Its fields are in the next table.

The data object

FieldTypeDescription
call_uuidstring
The call the key was pressed on, as the uuid on its call record. All zeros on the test sample.
Example: c7d2e1f0-4a3b-4c5d-8e9f-0a1b2c3d4e5f
from_numberstring
The number the caller called from, as the carrier presented it. It can arrive as +91 and ten digits, 91 and ten digits, 0091 and ten digits, 0 and ten digits, or the ten digits alone (today the wire carries 91 and the ten digits), so match on the last ten digits rather than on the whole string. +919876543210 on the test sample.
Example: 919812345678
to_numberstring
The number the caller dialled, as the carrier presented it and the edge passed it on (whitespace trimmed). Today that is 91 and the ten digits. Never match on the whole string; match on the last ten digits. On the test sample only it is your account's first number in the stored national form (0 and the ten digits), or empty when the account holds no number yet.
Example: 911100000042
flow_idnumber
The id of the call menu the key was pressed on.
Example: 128
flow_namestring
The name of that call menu, as shown in your dashboard.
Example: Main office menu
key_pathstring
Which key was pressed. On the main menu it is the digit alone, for example 1. On a sub-menu the path is dash-joined from the top, so 2-1 is key 1 on the menu that key 2 opened. One event is sent for each press, so a caller who reaches 2-1 produces an event for 2 and then one for 2-1. 1 on the test sample.
Example: 2-1
key_labelstring
The label you gave that key on the call menu. Empty when the key has no label. Sales on the test sample.
Example: Support
JSON
{
  "id": "evt_5b8e02d94c17a3f6e0d2",
  "type": "ivr.keypress",
  "event": "ivr.keypress",
  "created": 1780591342,
  "timestamp_ist": "2026-06-04T22:12:22.518+05:30",
  "test": false,
  "data": {
    "call_uuid": "c7d2e1f0-4a3b-4c5d-8e9f-0a1b2c3d4e5f",
    "from_number": "919812345678",
    "to_number": "911100000042",
    "flow_id": 128,
    "flow_name": "Main office menu",
    "key_path": "2-1",
    "key_label": "Support"
  }
}

The test sample

The Test button on the call menu page sends one signed sample to the address in the box (whether or not it has been saved yet) so you can check your receiver end to end. It is the same body with test true, an id prefixed evt_test_, a call_uuid of all zeros, the caller +919876543210, to_number your account's first number (or empty if you have not chosen one yet), and key 1 labelled Sales. It is one attempt with an 8 second window, and it does not touch the last delivery result shown on the page.

JSON
{
  "id": "evt_test_3a9c5e17b0d4f2861c7e",
  "type": "ivr.keypress",
  "event": "ivr.keypress",
  "created": 1780591402,
  "timestamp_ist": "2026-06-04T22:13:22.000+05:30",
  "test": true,
  "data": {
    "call_uuid": "00000000-0000-0000-0000-000000000000",
    "from_number": "+919876543210",
    "to_number": "01100000042",
    "flow_id": 128,
    "flow_name": "Main office menu",
    "key_path": "1",
    "key_label": "Sales"
  }
}

Headers, verification and delivery

  • Headers. The same set as a campaign webhook, including both signatures. See Headers.
  • Verification. The same secret and the same routine: HMAC-SHA256 of the raw body for version 1, and of the timestamp, a dot and the raw body for version 2. See Verifying the signature.
  • Delivery. The same rule: one POST, one retry after 2 seconds on a 5xx, a timeout or an unreachable address, a 4xx is final, only a 2xx counts, and at most one redirect is followed. See Delivery. The call menu page shows the last delivery result: the time, the HTTP status or that there was no answer, and the reason when it failed.

Voice OTP

Sending a one-time code over a phone call and verifying what the recipient enters is documented in its own reference, the Voice OTP API. It uses the same base URL, credentials, envelope, error codes and rate limits as this page and adds two endpoints:

  • POST /otp/send places a call that reads the code out and returns a request_id.
  • POST /otp/verify checks the code the recipient gives you, with expiry and attempt limits handled for you.

The otp type on Place a call is a shortcut to the same verification call.

Quick start

  1. Copy your credentials
    Open the API tab in your Voice dashboard and copy your publishable key and secret. Reveal the secret once and store it securely.
  2. Pick an audio clip
    Upload or choose a clip in your audio library and note its id.
  3. Place a test call to your own number
    cURL
    curl https://voice.agentive.co.in/api/public/v1/calls \
      -H "x-api-key: pk_live_..." \
      -H "x-api-secret: sk_live_..." \
      -H "Content-Type: application/json" \
      -d '{ "type": "audio_blast", "number": "98XXXXXXXX", "campaign_id": 4821, "reference_id": "test-1" }'
  4. Poll the result
    Note the unique_id from the response and check its status:
    cURL
    curl https://voice.agentive.co.in/api/public/v1/calls/run_XXXX \
      -H "x-api-key: pk_live_..." \
      -H "x-api-secret: sk_live_..."
  5. Go event-driven
    To receive call results automatically instead of polling, set up a webhook. Then send an Idempotency-Key on every billed request before you go live.

Next steps