Click to Call API
Click to Call API
Connect one of your team members and a customer with one request from your CRM or app. We ring your team member first, then the customer, and keep you posted as the call goes.
- Base URL
- https://voice.agentive.co.in/api/public/v1
- Format
- JSON over HTTPS
- Auth
- x-api-key + x-api-secret
Introduction
Click to call lets your CRM, helpdesk or app start a phone call between one of your team members and a customer with a single request. We ring your team member first, on the phone or app they already use with us. When they answer, we call the customer from your business number and put the two of them together.
You build no call screen. Your team member's phone, or our app or web panel, is the call screen: they hear the customer, see who it is, and use the same buttons they use for any call. Your system starts the call, follows it live, and can press keypad, hold, transfer and end if you want buttons of your own.
What you can do
- List your team members, see who can be rung right now, and map your own users to them by email.
- Start a call: your team member is rung first, then the customer.
- Follow the call through signed webhooks, or read it when you like.
- Play keypad tones, hold, resume, transfer to a colleague, a call menu or a number, and hang up.
- Receive the outcome your team member saves for the call, and download the recording.
feature_disabled, and the click_to_call.* events are not offered. GET /account tells you what your key can use: products.click_to_call is true when your account has the Telephony API. It does not say whether click to call is already switched on for your account: until it is, every route on this page answers HTTP 503 with service_unavailable and retryable: false (no Retry-After), before the Telephony API is checked. Do not retry that answer; ask your account manager. An account with the Telephony API reads and receives events for all its calls.insufficient_balance.Quick start
Four steps from keys to a call on your own phone.
- Get your keysIn the Voice dashboard, open Developers > API Access and copy your publishable key and secret. The secret is shown in full only once. Put both in environment variables on your server, for example
AGENTIVE_KEYandAGENTIVE_SECRET. - Find a team memberList the team members who can be rung right now, and note the
idoremailof yours.cURLcurl "https://voice.agentive.co.in/api/public/v1/members?ringable=true" \ -H "x-api-key: $AGENTIVE_KEY" \ -H "x-api-secret: $AGENTIVE_SECRET" - Start a callUse your own mobile number as
to. Your team member's app or panel rings; answer it, and your mobile rings next.cURLcurl https://voice.agentive.co.in/api/public/v1/click-to-call \ -H "x-api-key: $AGENTIVE_KEY" \ -H "x-api-secret: $AGENTIVE_SECRET" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: first-click-to-call-1" \ -d '{ "member_email": "neha@example.in", "to": "98XXXXXX21", "reference_id": "test-1" }'Keep theunique_idfrom the response and read the call while it runs:cURLcurl https://voice.agentive.co.in/api/public/v1/click-to-call/c2c_9f1c4e7a2b6d48e0a3c5f1d29b7e6a40 \ -H "x-api-key: $AGENTIVE_KEY" \ -H "x-api-secret: $AGENTIVE_SECRET" - Add a webhookUnder Developers > Webhooks, add an
https://endpoint and tick the Click to call events, so the call's progress comes to you instead of you asking for it. See Webhooks.
Platform conventions
Agentive ships two API families: the Voice APIs (Voice Broadcast, AI Voice Agent, Voice OTP and telephony webhooks) and the WhatsApp Business API. They share a brand, a dashboard account and a set of habits, but they are separate products and they do not agree on everything. This section is the one place that says what is common and, where they differ, exactly how.
Base URLs
| Voice APIs | WhatsApp Business API | |
|---|---|---|
| Base URL | https://voice.agentive.co.in/api/public/v1 | https://app.agentive.co.in/api/v1 |
| Covers | Voice Broadcast, AI Voice Agent, Voice OTP and the endpoints webhook events point back at. All of them share one base, one credential pair and one envelope. | Template sends for one API campaign. Issued and managed from the Chat dashboard. |
| Transport | HTTPS only, JSON in and JSON out. | HTTPS only, JSON in and JSON out. |
Authentication
| Voice APIs | WhatsApp Business API | |
|---|---|---|
| Credential | One publishable key (pk_live_...) and one secret (sk_live_...) per account. | One token (agcamp_...) per API campaign, so a leaked token can only send that campaign. |
| How to send it | x-api-key and x-api-secret headers, or HTTP Basic with the publishable key as the username and the secret as the password. | Authorization: Bearer <token>. |
| Scope | Account-wide. Every endpoint on the Voice base accepts it. | Campaign-wide. The template and number are fixed on the campaign. |
| Treat as | Both keys are confidential. The publishable key names your account to anyone who holds it, so keep it with the secret, on your server only. | A secret. Keep it on your server only. |
| Rotation | From the API Access tab of the Voice dashboard. By default the old secret stops working at once; for a planned change you can keep it working for 24 hours beside the new one. | From the campaign page in the Chat dashboard. The old token stops working at once. |
Error envelope
| Voice APIs | WhatsApp Business API | |
|---|---|---|
| Shape | { status, code: number, error_code: string, error, details, unique_id, reference_id } | { error, code: string, details: object } |
| What to branch on | error_code. The numeric code mirrors the HTTP status and stays for compatibility. | code, which is itself the machine string. |
| Human sentence | details, repeated in error so a handler written for the WhatsApp shape reads a Voice error too. | error. |
| Extra context | Named fields beside the envelope (lines, lines_in_use, attempts_left, retry_after_sec, balance_inr). | A details object (missing, retryAfter, meta, validation). |
401 and 403
| Voice APIs | WhatsApp Business API | |
|---|---|---|
| 401 | Credentials missing, unknown, or rejected. The status never confirms whether a key exists. | Token missing or wrong. |
| 403 | Credentials accepted, but the account is not active or the product is not enabled. | Token accepted, but the campaign is not live or the number is not connected. |
| 404 | Not found, or not yours. A foreign id is never a 403, because that would confirm it exists. | Not found, or not yours. Same rule. |
Idempotency
| Voice APIs | WhatsApp Business API | |
|---|---|---|
| Key | Idempotency-Key request header, 1 to 128 visible characters. A longer key is refused with HTTP 400. | externalId in the body (the Idempotency-Key header is accepted too), up to 160 characters. |
| Window | 24 hours per account. | The life of the campaign. |
| A repeat | Replays the stored response. The one exception is a 5xx raised after the call was already handed to the network: that reply is stored as call_state_unknown with a sentence appended telling you to check the call before retrying. A 400 or 404 is stored for the 24 hours too, so once you fix the request or the dashboard setting, send it with a new key. A 401 is never stored: the credentials are checked before the key is read. | HTTP 200 with duplicate: true and the earlier delivery's current status. |
| Replay marker | Idempotent-Replayed: true, plus the original idempotency-replayed: true header. | duplicate: true in the body. |
| Same key, different body | HTTP 422 with idempotency_conflict. | The first send stands; the second is reported as a duplicate. |
| Still in flight | HTTP 409 with idempotency_in_progress and Retry-After. | Not applicable. |
Rate limits and headers
| Voice APIs | WhatsApp Business API | |
|---|---|---|
| Budget | Per account: 120 reads a minute and 120 writes a minute, counted separately, so polling can never starve call placement. | Per campaign token: 300 requests a minute over a sliding window. |
| Limit headers | RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset, with X-RateLimit-* aliases of the same three values. | None. |
| How long to wait | Retry-After on every 429 and every 409, mirrored in the body as retry_after_sec. | Retry-After on rate_limited; details.retryAfter (IST) on quiet_hours. |
| Capacity refusal | HTTP 429 concurrency_limit when the account has no free line, with lines, lines_in_use and retry_after_sec in the body and a Retry-After of 5 seconds. Call-placing responses also carry X-Agentive-Lines and X-Agentive-Lines-In-Use. | Not applicable; there are no lines to exhaust. |
Webhook signing
| Voice APIs | WhatsApp Business API | |
|---|---|---|
| Header to verify | X-Agentive-Signature-V2: t=<unix seconds>,v2=<hex> | X-Agentive-Signature: <hex>, with no prefix. |
| What is signed | HMAC-SHA256, with the signing secret, of t, a dot, then the exact raw body. | HMAC-SHA256 of the exact raw body with the endpoint's signing secret. |
| Freshness | Reject a t more than 300 seconds from your own clock. The timestamp is inside the signed material, so a captured delivery cannot be sent again later. | Nothing time-bound is signed. De-duplicate on the event id for good, and accept deliveries over HTTPS only. |
| Comparison | Constant time, on bytes. Reject on any mismatch, and reject a delivery with no version 2 header. | Constant time, on bytes. Reject on any mismatch. |
X-Agentive-Signature-V2. Voice deliveries also carry the older body-only X-Agentive-Signature for receivers built before version 2; do not accept it on its own, because it proves nothing about when a body was sent. The WhatsApp signature is the body alone with no prefix. A shared handler needs one verifier per product.Webhook delivery headers
| Voice APIs | WhatsApp Business API | |
|---|---|---|
| Event name | X-Agentive-Event | X-Agentive-Event |
| Event id | X-Agentive-Event-Id, the same value as id in the body. Shared by every endpoint that receives the event. | X-Agentive-Delivery, the same value as id in the body. Despite the name it is the event id, not a per-endpoint delivery id. |
| Delivery id | X-Agentive-Delivery-Id (dlv_...), one per event per endpoint, stable across every retry. | None. X-Agentive-Delivery is the Chat product's name for the event id above. |
| Attempt number | X-Agentive-Attempt, 1-based, counting through the whole retry schedule. | None. |
| Attempt time | X-Agentive-Timestamp, Unix seconds at the moment of this attempt, the same value as t in the version 2 signature. Check the age against the signed t, never against this header alone. | X-Agentive-Timestamp, the same timestamp as the envelope. |
| User agent | Agentive-Webhooks/2026-06-01 | Agentive-Webhooks/2026-05-08, the Chat payload version. |
Webhook retries
| Voice APIs | WhatsApp Business API | |
|---|---|---|
| Attempts | Nine: three fast, then six spread out. | Three. |
| Schedule | 0.5 s and 2 s between the first three, then 1 min, 5 min, 30 min, 2 h, 6 h and 12 h. | 0 s, 2 s and 8 s. |
| Total window | About 20 hours, after which the delivery is marked failed. | About 10 seconds. |
| Timeout per attempt | 6 seconds. | 10 seconds. |
| What you see | The dashboard lists the latest 50 deliveries per endpoint with the event, the latest result, the number of tries and the time in IST. Each delivery is kept for 30 days, then deleted. | The campaign's Deliveries tab shows the message and its current status. |
Timestamps and amounts
| Voice APIs | WhatsApp Business API | |
|---|---|---|
| Time zone | Indian Standard Time, ISO 8601 with a +05:30 offset. The field is named per product: created_at_ist on a call, expires_at_ist on a verification, timestamp_ist on a webhook envelope. | timestamp on the webhook envelope is UTC ISO 8601. Schedules and quiet hours are set and reported in IST. |
| UTC alongside | created_at_utc on a call, and occurred_at (UTC) plus created (Unix seconds) on a webhook envelope. | The envelope timestamp is already UTC. |
| Money | Indian Rupees, always. balance_inr is a number, never a formatted string. | Indian Rupees, always. Amounts appear in the dashboard, not in the send API. |
Versioning and field case
| Voice APIs | WhatsApp Business API | |
|---|---|---|
| API version | In the path: /api/public/v1. | In the path: /api/v1. |
| Payload version | api_version on every webhook event. Currently 2026-06-01. | apiVersion on every webhook event. Currently 2026-05-08. |
| Compatibility | Fields are added, never removed or redefined. Ignore fields you do not recognise. | Fields are added, never removed or redefined. Ignore fields you do not recognise. |
| Field case | snake_case throughout, request and response. | camelCase throughout, request and response. |
Security
- Every credential is a secret. That includes the Voice publishable key, whatever its name suggests: treat it exactly like the secret. Keep all keys on your server, in an environment variable or a secrets store, never in a browser, a mobile app, a spreadsheet or source control.
- Send them only to the API. Never forward a key or a secret to any other host, and do not let an HTTP client carry them across a redirect to another host.
- Rotate at once if one leaks. Rotate the Voice pair from the API Access tab of the Voice dashboard and a WhatsApp token from its campaign page. A leaked value must stop working immediately, so update your server in the same step. (For a planned Voice change, and only then, you can keep the old secret working for 24 hours.)
- HTTPS only. Write
https://in the base URL in your code. A request sent over plain HTTP has already exposed its credentials before any redirect can protect it. - No address allow-listing today. Requests are not restricted to your server's IP addresses, and webhooks are not sent from a fixed list of addresses. Authenticate every webhook by its signature, never by where it came from.
Authentication
Every path on this page is relative to this base URL. Use https://; a request over plain HTTP exposes your keys before anything can protect them.
https://voice.agentive.co.in/api/public/v1Every request carries two keys from Developers > API Access in the Voice dashboard, sent as two headers:
Headers
| Header | Type | Required | Description |
|---|---|---|---|
| x-api-key | string | Required | Your publishable key ( pk_live_...). Treat it as confidential. See Authentication. |
| x-api-secret | string | Required | Your secret key ( sk_live_...). Server-side only. |
curl https://voice.agentive.co.in/api/public/v1/members \
-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 recording download uses, because an HTTP client never carries an Authorization header to another host.
curl https://voice.agentive.co.in/api/public/v1/members \
-u "$AGENTIVE_KEY:$AGENTIVE_SECRET"- The keys can start calls from your team to any number, so keep both on your server: never in a web page, a mobile app, a browser extension or source control. A CRM page or an app asks your server, and your server asks us. See Integrating.
- Treat the publishable key like the secret, despite its name. The keys work for the whole account.
- Send the keys only to
voice.agentive.co.in, never to any other host. - If a key may have leaked, rotate it from API Access with Stop the old secret now (this also gives you a new publishable key). See Rotating your secret.
Authentication failures
| Situation | HTTP | error_code |
|---|---|---|
| No publishable key sent | 401 | missing_credentials |
| Unknown key, or a wrong secret | 401 | invalid_credentials |
| The account is not active | 403 | account_inactive |
| API access is not enabled for the account | 403 | api_disabled |
| The Telephony API is not on the account | 403 | feature_disabled |
401 always means the keys were rejected. 403 always means they were accepted and the account is not entitled. A refusal about the call itself (a blocked number, outgoing calls switched off, verification pending) is a 422, never a 403.
How the team member is rung
Your team member is always rung first, on the lane they chose in their own settings. The API never chooses the lane and never changes it. calling_mode on List team members tells you which lane their next call takes, and mode on the click to call says which one it took.
| Over the internet (internet) | Phone Bridge on (phone_bridge) | |
|---|---|---|
| Rings first | Their web Agent Panel (marked Click to call) and the Agentive app on their phone, like an incoming call. | Their own mobile number, over the phone network. |
| When they answer | We call the customer into the call they are already in, exactly as if they had pressed Call themselves. They hear the customer's phone ringing. | We call the customer and connect them, exactly as Phone Bridge works for any call they make. |
| How long they are rung | ring_timeout_sec: 30 seconds unless you send between 10 and 60. | About 28 seconds, set by the phone network. |
| During the call | The normal outgoing call bar: hold, keypad, transfer, end. Listed in Call History, and recorded when call recording is on for your account, like any call they place. | A normal phone call on their mobile. Listed in Call History, and recorded when call recording is on, like any Phone Bridge call. |
| Lines used | Not counted toward your account's line limit, like any call a team member places from the panel or app. | One toward your line limit when it is placed. While it is live, an incoming call counts it as two (one to the team member, one to the customer) when we check whether the incoming call fits. |
What your team member sees
- The ring shows the customer's name from your contacts, or else the
customer_nameyou sent, or else the number. A team member set to see customer numbers hidden sees the number hidden, as everywhere else. - If they decline, or do not answer in time, nothing more happens: the customer is never called, and the click to call ends as
member_declinedormember_no_answer. - After the call they can save an outcome, a note and a callback time, as for any call; you receive it as
click_to_call.tagged. - When your system holds the call, the web panel shows it as Held.
member_unavailable or member_on_call, and nothing rings anywhere.Integrating
The keys never leave your server, wherever the Call button is. Your page or app asks your server, your server asks us, and the progress comes back to your server as webhooks.
From a web CRM
- Map your users onceCall List team members and match each of your users to a team member by email. Keep the member
id, or sendmember_emaileach time. - Add a Call buttonThe button calls your own server with the record it belongs to. Your server looks up the customer's number in your own records and calls Start a click to call with the signed-in user's email, the number, your record id as
reference_idand a freshIdempotency-Keyper click. - Show the progressPush the
click_to_call.*events from your webhook to the page, or poll your server, which reads the click to call every 3 seconds at most. - Write the result backOn
click_to_call.ended, store theoutcomeandduration_secon the record. Onclick_to_call.tagged, store your team member's outcome and note.
From your own mobile app
Your app never holds the keys: anything inside an app can be read by whoever installs it. The app calls your server, your server calls us, and your team member's phone rings.
Your app Your server Agentive API Team member
| | | |
| tap "Call" | | |
| (lead id) --------->| | |
| | POST /click-to-call | |
| | (member_email, to) ---->| |
| | | rings their phone, |
| |<--- 201 c2c_... | app or web panel ----->|
|<--- "Ringing Neha" | member_ringing | |
| | |<------------- answers |
| |<--- webhook events | calls the customer |
|<--- live status | (signed) | into their call |
| | | |- The app sends your own record id (a lead, a ticket), never a phone number, so a tampered app cannot make your team call anyone it likes.
- Your server knows who is signed in to the app, and sends that person's email as
member_email. - The app learns the state from your server: poll it, or push the webhook state to the app.
import express from "express";
const API = "https://voice.agentive.co.in/api/public/v1";
const KEY = process.env.AGENTIVE_KEY;
const SECRET = process.env.AGENTIVE_SECRET;
if (!KEY || !SECRET) throw new Error("AGENTIVE_KEY and AGENTIVE_SECRET must be set");
const app = express();
app.use(express.json());
async function agentive(path, { method = "GET", body, idempotencyKey } = {}) {
const res = await fetch(`${API}${path}`, {
method,
headers: {
"x-api-key": KEY,
"x-api-secret": SECRET,
...(body ? { "Content-Type": "application/json" } : {}),
...(idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
return { status: res.status, data: await res.json() };
}
// requireAppUser and db are your own: your app's sign-in check and your records.
// The app sends a lead id, never a phone number: the number comes from your own
// records, so a tampered app cannot make your team call anyone it likes.
app.post("/calls", requireAppUser, async (req, res) => {
const lead = await db.leads.findForUser(req.user.id, req.body.lead_id);
if (!lead) return res.status(404).json({ error: "Lead not found" });
const { status, data } = await agentive("/click-to-call", {
method: "POST",
// One key per tap, so a network retry of this request never rings twice.
idempotencyKey: `lead-${lead.id}-tap-${req.body.tap_id}`,
body: {
member_email: req.user.email, // your user is the team member with this email
to: lead.phone,
customer_name: lead.name,
reference_id: `lead-${lead.id}`,
},
});
if (data.status !== "success") {
// Show the sentence, branch on error_code, offer a retry only when retryable.
return res.status(status).json({ error: data.details, code: data.error_code, retryable: data.retryable === true });
}
await db.calls.save({ userId: req.user.id, leadId: lead.id, clickToCallId: data.unique_id });
res.status(201).json({ id: data.unique_id, state: data.click_to_call.state });
});
// The app polls this every 3 seconds at most, or you push the webhook state to it.
app.get("/calls/:id", requireAppUser, async (req, res) => {
const call = await db.calls.findForUser(req.user.id, req.params.id);
if (!call) return res.status(404).json({ error: "Not found" });
const { data } = await agentive(`/click-to-call/${encodeURIComponent(call.clickToCallId)}`);
const c = data.click_to_call;
res.json({ state: c.state, active: c.active, on_hold: c.on_hold, outcome: c.outcome });
});
app.listen(3000);Endpoints
| Endpoint | Purpose |
|---|---|
| GET /members | Your team members, and whether each can be rung now. |
| GET /members/{member_id} | One team member. |
| GET /call-menus | Your call menus, as transfer targets. |
| POST /click-to-call | Ring a team member, then the customer. |
| GET /click-to-call/{id} | Read one click to call. |
| GET /click-to-call | Page through click to calls, or catch up after a missed webhook. |
| POST /click-to-call/{id}/hangup | Cancel it, or end the conversation. |
| POST /click-to-call/{id}/dtmf | Play keypad tones into the call. |
| POST /click-to-call/{id}/hold | Put the customer on hold. |
| POST /click-to-call/{id}/unhold | Take the customer off hold. |
| POST /click-to-call/{id}/transfer | Hand the customer to a colleague, a call menu or a number. |
| GET /calls/{call_id}/recording | The recording, streamed, and each of its parts. |
List team members
/membersYour team members, with whether each can be rung through this API at all (can_click_to_call) and right now (ringable_now), the lane their next call takes and the numbers they can call from. Platform staff are never listed.
Headers
| Header | Type | Required | Description |
|---|---|---|---|
| x-api-key | string | Required | Your publishable key ( pk_live_...). Treat it as confidential. See Authentication. |
| x-api-secret | string | Required | Your secret key ( sk_live_...). Server-side only. |
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| q | string | Optional | Part of a name or an email. Example: neha |
| ringable | boolean | Optional | true lists only the team members a click to call would ring now. |
| page | number | Optional | The page of results to return. Default 1. |
| per_page | number | Optional | Results per page. Default 50, maximum 100. |
Request
curl "https://voice.agentive.co.in/api/public/v1/members?q=neha&per_page=20" \
-H "x-api-key: $AGENTIVE_KEY" \
-H "x-api-secret: $AGENTIVE_SECRET"Response
{
"status": "success",
"code": 200,
"data": [
{
"id": 1042,
"object": "member",
"name": "Neha Sharma",
"email": "neha@example.in",
"role": "agent",
"calling_mode": "internet",
"presence": "available",
"on_call": false,
"current_click_to_call_id": null,
"can_click_to_call": true,
"unavailable_reason": null,
"ringable_now": true,
"not_ringable_reason": null,
"numbers": [
{
"number": "+9111XXXXXX45",
"shared": true,
"default": true
}
],
"mobile_last4": null
},
{
"id": 1057,
"object": "member",
"name": "Arjun Mehta",
"email": "arjun@example.in",
"role": "agent",
"calling_mode": "phone_bridge",
"presence": "available",
"on_call": true,
"current_click_to_call_id": "c2c_4d7b1e9c3a5f42d8b6e0c2a1f9d37b58",
"can_click_to_call": true,
"unavailable_reason": null,
"ringable_now": false,
"not_ringable_reason": "on_call",
"numbers": [
{
"number": "+9179XXXXXX12",
"shared": false,
"default": true
}
],
"mobile_last4": "4521"
}
],
"pagination": {
"page": 1,
"per_page": 50,
"total": 2,
"total_pages": 1
}
}Response fields
| Field | Type | Description |
|---|---|---|
| data[].id | number | The member_id to send. |
| data[].object | string | Always member. |
| data[].name, data[].email | string | As set in Team Members. Map your own users to team members by this email. |
| data[].role | string | org_admin (an account admin), manager, agent or viewer. |
| data[].calling_mode | string | internet or phone_bridge: the lane the next call would take. The team member's own setting; see How the team member is rung. |
| data[].presence | string | available, break or offline, as in the Agent Panel. A member who went offline automatically while still signed in on a device reads available, as for incoming calls. |
| data[].on_call | boolean | On a call right now, or being rung. |
| data[].current_click_to_call_id | string | null | The click to call they are on, when there is one. |
| data[].can_click_to_call | boolean | Whether the member can be rung through this API at all. |
| data[].unavailable_reason | string | null | Why not, when can_click_to_call is false: no_calling_number, app_agent, paused, mobile_not_verified or not_a_calling_role. |
| data[].ringable_now | boolean | Whether a click to call started now would ring them. |
| data[].not_ringable_reason | string | null | Why not, when ringable_now is false: offline, on_break, no_device (no web panel or app signed in), on_call or cooldown. |
| data[].numbers | object[] | The numbers they can call from: number (E.164), shared (a team line) and default (used when you send no caller_id). |
| data[].mobile_last4 | string | null | The last four digits of their verified mobile number, or null. The full number is never returned. |
Errors
| Status | Code | error_code | When | What to do |
|---|---|---|---|---|
| 403 | 403 | feature_disabled | Your account does not have the Telephony API, or (on a transfer) the kind of transfer you asked for: transfer to a call menu, or to an outside number. | Contact your account manager. |
| 429 | 429 | rate_limited | Your request budget, the reading budget of a live click to call, or your account's calls a minute, is spent. | Wait for Retry-After. |
Get a team member
/members/{member_id}One team member, in the same shape as the list, inside data.
Headers
| Header | Type | Required | Description |
|---|---|---|---|
| x-api-key | string | Required | Your publishable key ( pk_live_...). Treat it as confidential. See Authentication. |
| x-api-secret | string | Required | Your secret key ( sk_live_...). Server-side only. |
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| member_id | number | Required | The team member's id. Example: 1042 |
Request
curl https://voice.agentive.co.in/api/public/v1/members/1042 \
-H "x-api-key: $AGENTIVE_KEY" \
-H "x-api-secret: $AGENTIVE_SECRET"Response
{
"status": "success",
"code": 200,
"data": {
"id": 1042,
"object": "member",
"name": "Neha Sharma",
"email": "neha@example.in",
"role": "agent",
"calling_mode": "internet",
"presence": "available",
"on_call": false,
"current_click_to_call_id": null,
"can_click_to_call": true,
"unavailable_reason": null,
"ringable_now": true,
"not_ringable_reason": null,
"numbers": [
{
"number": "+9111XXXXXX45",
"shared": true,
"default": true
}
],
"mobile_last4": null
}
}Errors
| Status | Code | error_code | When | What to do |
|---|---|---|---|---|
| 404 | 404 | member_not_found | No team member with that id or email on your account. Also answered for a colleague named on a transfer. | Check the id or email with List team members. |
| 403 | 403 | feature_disabled | Your account does not have the Telephony API, or (on a transfer) the kind of transfer you asked for: transfer to a call menu, or to an outside number. | Contact your account manager. |
| 429 | 429 | rate_limited | Your request budget, the reading budget of a live click to call, or your account's calls a minute, is spent. | Wait for Retry-After. |
Start a click to call
/click-to-callRing a team member, then the customer. Every check runs before anything rings: the team member, their presence and their limits, the number, your account's settings and limits. If one fails, the request is refused, nothing rings, and no click to call is created. If they all pass, the answer is 201 with the click to call in member_ringing, and its id as unique_id. Unknown fields are refused, so a typo never silently does something else.
Headers
| Header | Type | Required | Description |
|---|---|---|---|
| x-api-key | string | Required | Your publishable key ( pk_live_...). Treat it as confidential. See Authentication. |
| x-api-secret | string | Required | Your secret key ( sk_live_...). Server-side only. |
| Content-Type | string | Required | Always application/json. |
| Idempotency-Key | string | Optional | A key unique to this request, 1 to 128 visible ASCII characters. Send one on every POST: a retry with the same key returns the first answer and never rings anyone twice. See Idempotency.Example: lead-2291-call-1 |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| member_id | number | Optional | The team member to ring. Send exactly one of member_id and member_email.Example: 1042 |
| member_email | string | Optional | The team member's email in Team Members, instead of member_id. Example: neha@example.in |
| to | string | Required | The customer: an Indian number in any common form, 98XXXXXX21, +91 98XXX XXX21, 098XXXXXX21, or a landline with its code. Never one of your own account's numbers.Example: 98XXXXXX21 |
| caller_id | string | Optional | The number the customer sees: one of the team member's numbers, matched on its last ten digits. Leave it out to use their default.Example: 011XXXXXX45 |
| customer_name | string | Optional | Shown on the team member's ring when your contacts have no name for the number. Up to 60 characters; control and text-direction characters are removed first. Example: Ravi Kumar |
| ring_timeout_sec | number | Optional | How long the team member's app and panel ring, 10 to 60. Default 30. A Phone Bridge mobile rings for about 28 seconds whatever you send. Example: 30 |
| reference_id | string | Optional | Your own id for the call, up to 120 characters and no control characters. Echoed on every answer and every event. It never de-duplicates anything; use an Idempotency-Key for that.Example: LEAD-2291 |
| metadata | object | Optional | Your own data. Up to 10 keys of 1 to 40 letters, digits and _; each value a string or a number, up to 200 characters, no control characters. Echoed on every answer and event, never spoken. Do not put anything secret in it.Example: { "deal": "D-77" } |
| dry_run | boolean | Optional | true runs every check and answers 200 with the team member, the lane and the caller ID, without ringing anyone. See Dry run. |
Request
curl https://voice.agentive.co.in/api/public/v1/click-to-call \
-H "x-api-key: $AGENTIVE_KEY" \
-H "x-api-secret: $AGENTIVE_SECRET" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: lead-2291-call-1" \
-d '{
"member_email": "neha@example.in",
"to": "98XXXXXX21",
"customer_name": "Ravi Kumar",
"reference_id": "LEAD-2291",
"metadata": { "deal": "D-77", "crm_owner": "neha" }
}'Response (201)
{
"status": "success",
"code": 201,
"unique_id": "c2c_9f1c4e7a2b6d48e0a3c5f1d29b7e6a40",
"reference_id": "LEAD-2291",
"details": "Ringing the team member.",
"click_to_call": {
"id": "c2c_9f1c4e7a2b6d48e0a3c5f1d29b7e6a40",
"object": "click_to_call",
"state": "member_ringing",
"active": true,
"on_hold": false,
"mode": "internet",
"member": {
"id": 1042,
"name": "Neha Sharma"
},
"from_number": "+9111XXXXXX45",
"to_number": "+9198XXXXXX21",
"customer_name": "Ravi Kumar",
"call_id": null,
"reference_id": "LEAD-2291",
"metadata": {
"deal": "D-77",
"crm_owner": "neha"
},
"created_at": "2026-10-12T05:31:02.000Z",
"created_at_ist": "2026-10-12T11:01:02.000+05:30",
"member_answered_at": null,
"member_answered_at_ist": null,
"customer_answered_at": null,
"customer_answered_at_ist": null,
"ended_at": null,
"ended_at_ist": null,
"duration_sec": 0,
"outcome": null,
"ended_by": null,
"end_reason": null,
"transferred_to": null,
"recording_available": false,
"recording_url": null,
"disposition": null,
"sequence": 1,
"updated_at": "2026-10-12T05:31:02.000Z",
"updated_at_ist": "2026-10-12T11:01:02.000+05:30",
"dial_started_at": null,
"dial_started_at_ist": null,
"customer_ringing_at": null,
"customer_ringing_at_ist": null,
"member_ring_sec": null,
"customer_ring_sec": null,
"call_duration_sec": null,
"recording": {
"status": "none",
"revision": 0,
"url": null,
"format": null,
"size_bytes": null,
"duration_sec": null,
"channels": null,
"channel_layout": null,
"retention_days": 60,
"expires_at": "2026-12-11T05:31:02.000Z",
"parts": [],
"parts_ordered": false
}
}
}Response fields
| Field | Type | Description |
|---|---|---|
| unique_id | string | The click to call id. Keep it: every read and every event for this call carries it. |
| details | string | Ringing the team member. |
| click_to_call | object | The click to call object, in member_ringing. |
Errors
| Status | Code | error_code | When | What to do |
|---|---|---|---|---|
| 400 | 400 | validation_error | A field is missing, has the wrong type or breaks a rule, or the body carries a field the API does not take. field names it, for example agent_id in "Unknown field: agent_id". | Fix the request, then send it with a new Idempotency-Key (a 400 is kept for the key). |
| 400 | 400 | invalid_number | to (or to_number on a transfer) is not a number we can dial in India, or it is one of your own account's numbers. | Fix the number. |
| 400 | 400 | international_not_supported | The number is outside India, or your account's filter for numbers outside India refuses it. | Call only Indian numbers. |
| 400 | 400 | invalid_caller_id | caller_id is not one of the team member's numbers. | Send one of the member's numbers, or leave it out to use their default. |
| 404 | 404 | member_not_found | No team member with that id or email on your account. Also answered for a colleague named on a transfer. | Check the id or email with List team members. |
| 422 | 422 | member_cannot_call | The team member cannot place calls at all. details names why: no calling number, an app-only member, a paused member, a Phone Bridge mobile number that is not verified, or a role that does not place calls (unavailable_reason on the member says the same). | Fix it in Team Members, or pick another member. |
| 409 | 409 | member_unavailable | The team member is Offline or on a break, or (calls over the internet) has no web panel or app signed in. details says which. Nothing rang. | Ask them to go Available or sign in, or call another member. Retry after Retry-After. |
| 409 | 409 | member_on_call | The team member is on a call, or is already being rung by another click to call or a campaign. | Retry after Retry-After, when `ringable_now` on the member is true again. |
| 429 | 429 | member_cooldown | The team member missed or declined two click to calls in a row in the last 10 minutes. | Wait for Retry-After (when the cooldown ends), or call another member. |
| 429 | 429 | member_limit | The team member was rung 60 times in the last hour, or 300 times today (IST). | Wait for Retry-After, or call another member. |
| 429 | 429 | number_flood | 3 calls through the API to this number in 10 minutes, or 10 today (IST), counted across every API that places calls. Both limits answer this one code; details says which. | Wait for Retry-After (up to 600 seconds, or until midnight IST). Do not retry sooner. |
| 429 | 429 | rate_limited | Your request budget, the reading budget of a live click to call, or your account's calls a minute, is spent. | Wait for Retry-After. |
| 429 | 429 | concurrency_limit | Your account's line limit is reached by the calls live right now. A Phone Bridge click to call counts as one line when it is placed; a click to call over the internet is not counted, like any call a team member places from the panel or app. | Retry after Retry-After. |
| 402 | 402 | insufficient_balance | Your account's outgoing minutes for today are used up and its balance cannot cover more. No Retry-After is sent: it clears when you top up. | Top up, then retry with the same Idempotency-Key. |
| 422 | 422 | number_blocked | The number is on the do not disturb list, or on your account's or the team member's block list. | Do not call this number. |
| 422 | 422 | outbound_blocked | Outgoing calls are switched off on your account, the account is paused, or its plan has ended. | Contact your account manager. |
| 422 | 422 | verification_required | Your account cannot place calls until its business verification is complete. | Finish business verification in the dashboard. |
| 403 | 403 | feature_disabled | Your account does not have the Telephony API, or (on a transfer) the kind of transfer you asked for: transfer to a call menu, or to an outside number. | Contact your account manager. |
| 409 | 409 | idempotency_in_progress | A request with the same Idempotency-Key is still running. | Wait for Retry-After and send it again with the same key. |
| 422 | 422 | idempotency_conflict | The same Idempotency-Key was sent with a different body. | Use a new key for a new request. |
| 502 | 502 | call_not_placed | We could not place the call, or carry out the control, on our side. On a start, a keypad or a transfer request the Idempotency-Key is then used up: sending it again replays call_state_unknown. A start refused this way carries no unique_id. | Read the click to call (on a start, list by member_id or reference_id with created_from) before you try again, then use a new Idempotency-Key. |
| 503 | 503 | service_unavailable | The line for this caller ID cannot be reached right now, the call could not be reached to carry out a control, or the record could not be read. No Retry-After is sent. When the body says retryable: false (details "Click to call is switched off for this account."), click to call is not switched on for your account right now: every route answers this, before the Telephony API is checked. | Wait about 30 seconds (add some jitter), then try again. After a keypad or transfer request we cannot be sure nothing was done: read the click to call first and use a new Idempotency-Key. If retryable is false, do not retry: contact your account manager. |
HTTP/1.1 400 Bad Request
{
"status": "error",
"code": 400,
"unique_id": null,
"reference_id": "LEAD-2291",
"details": "Unknown field: agent_id",
"error_code": "validation_error",
"error": "Unknown field: agent_id",
"retryable": false,
"field": "agent_id"
}HTTP/1.1 429 Too Many Requests
Retry-After: 412
{
"status": "error",
"code": 429,
"unique_id": null,
"reference_id": "LEAD-2291",
"details": "This team member missed or declined two calls in a row. Try again in a few minutes, or call another member.",
"error_code": "member_cooldown",
"error": "This team member missed or declined two calls in a row. Try again in a few minutes, or call another member.",
"retryable": true,
"retry_after_sec": 412
}HTTP/1.1 422 Unprocessable Entity
{
"status": "error",
"code": 422,
"unique_id": null,
"reference_id": "LEAD-2291",
"details": "This number cannot be called.",
"error_code": "number_blocked",
"error": "This number cannot be called.",
"retryable": false
}ringable_now on Get a team member.Get a click to call
/click-to-call/{id}Read one click to call, always from our current record. Poll every 3 seconds at most, and stop once active is false. A live click to call has its own reading budget, so polling it does not use up your account's reads; see Limits and polling. Webhooks are the better way to wait.
Headers
| Header | Type | Required | Description |
|---|---|---|---|
| x-api-key | string | Required | Your publishable key ( pk_live_...). Treat it as confidential. See Authentication. |
| x-api-secret | string | Required | Your secret key ( sk_live_...). Server-side only. |
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| id | string | Required | The click to call id, c2c_ and 32 hex characters, as returned in unique_id when you started it. Another account's id answers 404.Example: c2c_9f1c4e7a2b6d48e0a3c5f1d29b7e6a40 |
Request
curl https://voice.agentive.co.in/api/public/v1/click-to-call/c2c_9f1c4e7a2b6d48e0a3c5f1d29b7e6a40 \
-H "x-api-key: $AGENTIVE_KEY" \
-H "x-api-secret: $AGENTIVE_SECRET"Response
{
"status": "success",
"code": 200,
"unique_id": "c2c_9f1c4e7a2b6d48e0a3c5f1d29b7e6a40",
"reference_id": "LEAD-2291",
"details": "in_progress",
"click_to_call": {
"id": "c2c_9f1c4e7a2b6d48e0a3c5f1d29b7e6a40",
"object": "click_to_call",
"state": "in_progress",
"active": true,
"on_hold": false,
"mode": "internet",
"member": {
"id": 1042,
"name": "Neha Sharma"
},
"from_number": "+9111XXXXXX45",
"to_number": "+9198XXXXXX21",
"customer_name": "Ravi Kumar",
"call_id": "6b1e2f3a-4c5d-4e6f-8a7b-9c0d1e2f3a4b",
"reference_id": "LEAD-2291",
"metadata": {
"deal": "D-77",
"crm_owner": "neha"
},
"created_at": "2026-10-12T05:31:02.000Z",
"created_at_ist": "2026-10-12T11:01:02.000+05:30",
"member_answered_at": "2026-10-12T05:31:09.204Z",
"member_answered_at_ist": "2026-10-12T11:01:09.204+05:30",
"customer_answered_at": "2026-10-12T05:31:19.516Z",
"customer_answered_at_ist": "2026-10-12T11:01:19.516+05:30",
"ended_at": null,
"ended_at_ist": null,
"duration_sec": 42,
"outcome": null,
"ended_by": null,
"end_reason": null,
"transferred_to": null,
"recording_available": false,
"recording_url": null,
"disposition": null,
"sequence": 4,
"updated_at": "2026-10-12T05:31:19.516Z",
"updated_at_ist": "2026-10-12T11:01:19.516+05:30",
"dial_started_at": "2026-10-12T05:31:10.000Z",
"dial_started_at_ist": "2026-10-12T11:01:10.000+05:30",
"customer_ringing_at": "2026-10-12T05:31:10.400Z",
"customer_ringing_at_ist": "2026-10-12T11:01:10.400+05:30",
"member_ring_sec": 7,
"customer_ring_sec": 9,
"call_duration_sec": null,
"recording": {
"status": "processing",
"revision": 0,
"url": null,
"format": null,
"size_bytes": null,
"duration_sec": null,
"channels": null,
"channel_layout": null,
"retention_days": 60,
"expires_at": "2026-12-11T05:31:10.000Z",
"parts": [],
"parts_ordered": false
}
}
}Response fields
| Field | Type | Description |
|---|---|---|
| unique_id | string | The click to call id. |
| details | string | The state, as a word. |
| click_to_call | object | The click to call object. |
Errors
| Status | Code | error_code | When | What to do |
|---|---|---|---|---|
| 404 | 404 | not_found | No click to call with that id on your account. | Check the id. |
| 403 | 403 | feature_disabled | Your account does not have the Telephony API, or (on a transfer) the kind of transfer you asked for: transfer to a call menu, or to an outside number. | Contact your account manager. |
| 429 | 429 | rate_limited | Your request budget, the reading budget of a live click to call, or your account's calls a minute, is spent. | Wait for Retry-After. |
GET /calls/{id} also accepts a c2c_ id and answers the call record. Its answered_at is usually the call record's own, to the second, after a transfer too, while the click to call's customer_answered_at carries milliseconds, so the two can differ by up to a second. Only before the call record exists is its answered_at the click to call's customer_answered_at itself. The click to call's ended_at usually carries milliseconds too (since 14 October 2026), so it can read up to a second later than the call record's ended_at. A call that ends in the same second as the customer's answer (or, on Phone Bridge, the team member's) never reads an end earlier than that answer.List click to calls
/click-to-callYour click to calls, newest first. With updated_since they are ordered by when they last changed instead, oldest change first, to catch up on anything a webhook did not bring you: pass pagination.next_cursor as cursor with the same filters until next_cursor is null. A click to call that changes during your walk moves to the end and is read again, never skipped. For the next catch-up, pass the highest updated_at you read as updated_since, and keep the copy with the highest sequence. A query parameter we do not know is reported in warnings and the X-Agentive-Warning header, and will later be refused.
Headers
| Header | Type | Required | Description |
|---|---|---|---|
| x-api-key | string | Required | Your publishable key ( pk_live_...). Treat it as confidential. See Authentication. |
| x-api-secret | string | Required | Your secret key ( sk_live_...). Server-side only. |
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| state | string | Optional | Only this state. |
| active | boolean | Optional | true for calls still going, false for ended ones. |
| member_id | number | Optional | Only this team member's. |
| reference_id | string | Optional | Only those with this reference_id. |
| created_from, created_to | string | Optional | ISO 8601. Started at or after created_from, and at or before created_to (both ends included). Example: 2026-10-12T00:00:00+05:30 |
| updated_since | string | Optional | ISO 8601. Only those that changed at or after this time, oldest change first. Example: 2026-10-12T05:00:00Z |
| cursor | string | Optional | The pagination.next_cursor of the previous page, sent with the same filters. A page read with a cursor carries null page, total and total_pages. |
| page | number | Optional | The page of results to return. Default 1. |
| per_page | number | Optional | Results per page. Default 50, maximum 100. |
Request
# Everything that changed since your last successful sync, to catch a missed webhook.
curl "https://voice.agentive.co.in/api/public/v1/click-to-call?updated_since=2026-10-12T05:00:00Z&per_page=100" \
-H "x-api-key: $AGENTIVE_KEY" \
-H "x-api-secret: $AGENTIVE_SECRET"Response
{
"status": "success",
"code": 200,
"data": [
{
"id": "c2c_9f1c4e7a2b6d48e0a3c5f1d29b7e6a40",
"object": "click_to_call",
"state": "completed",
"active": false,
"on_hold": false,
"mode": "internet",
"member": {
"id": 1042,
"name": "Neha Sharma"
},
"from_number": "+9111XXXXXX45",
"to_number": "+9198XXXXXX21",
"customer_name": "Ravi Kumar",
"call_id": "6b1e2f3a-4c5d-4e6f-8a7b-9c0d1e2f3a4b",
"reference_id": "LEAD-2291",
"metadata": {
"deal": "D-77",
"crm_owner": "neha"
},
"created_at": "2026-10-12T05:31:02.000Z",
"created_at_ist": "2026-10-12T11:01:02.000+05:30",
"member_answered_at": "2026-10-12T05:31:09.204Z",
"member_answered_at_ist": "2026-10-12T11:01:09.204+05:30",
"customer_answered_at": "2026-10-12T05:31:19.516Z",
"customer_answered_at_ist": "2026-10-12T11:01:19.516+05:30",
"ended_at": "2026-10-12T05:32:53.000Z",
"ended_at_ist": "2026-10-12T11:02:53.000+05:30",
"duration_sec": 94,
"outcome": "completed",
"ended_by": "customer",
"end_reason": "customer_ended",
"transferred_to": null,
"recording_available": true,
"recording_url": "https://voice.agentive.co.in/api/public/v1/calls/6b1e2f3a-4c5d-4e6f-8a7b-9c0d1e2f3a4b/recording",
"disposition": {
"outcome": "callback",
"note": "Wants the Gold plan quote on email first.",
"callback_at": "2026-10-14T05:30:00.000Z",
"tagged_at": "2026-10-12T05:34:05.000Z"
},
"sequence": 8,
"updated_at": "2026-10-12T05:34:05.000Z",
"updated_at_ist": "2026-10-12T11:04:05.000+05:30",
"dial_started_at": "2026-10-12T05:31:10.000Z",
"dial_started_at_ist": "2026-10-12T11:01:10.000+05:30",
"customer_ringing_at": "2026-10-12T05:31:10.400Z",
"customer_ringing_at_ist": "2026-10-12T11:01:10.400+05:30",
"member_ring_sec": 7,
"customer_ring_sec": 9,
"call_duration_sec": 94,
"recording": {
"status": "available",
"revision": 1,
"url": "https://voice.agentive.co.in/api/public/v1/calls/6b1e2f3a-4c5d-4e6f-8a7b-9c0d1e2f3a4b/recording",
"format": "ogg",
"size_bytes": 381952,
"duration_sec": 94.6,
"channels": 2,
"channel_layout": {
"left": "customer",
"right": "member"
},
"retention_days": 60,
"expires_at": "2026-12-11T05:31:10.000Z",
"parts": [
{
"part": 1,
"primary": true,
"segment": null,
"url": "https://voice.agentive.co.in/api/public/v1/calls/6b1e2f3a-4c5d-4e6f-8a7b-9c0d1e2f3a4b/recording",
"format": "ogg",
"size_bytes": 381952,
"duration_sec": 94.6,
"channels": 2,
"started_at": "2026-10-12T05:31:19.516Z",
"member": null
}
],
"parts_ordered": true
}
},
{
"id": "c2c_4d7b1e9c3a5f42d8b6e0c2a1f9d37b58",
"object": "click_to_call",
"state": "transferred",
"active": false,
"on_hold": false,
"mode": "phone_bridge",
"member": {
"id": 1057,
"name": "Arjun Mehta"
},
"from_number": "+9179XXXXXX12",
"to_number": "+9197XXXXXX08",
"customer_name": "Meera Iyer",
"call_id": "8d3f4b5c-6e7a-4b8c-9d0e-1f2a3b4c5d6e",
"reference_id": "LEAD-2304",
"metadata": {
"deal": "D-81"
},
"created_at": "2026-10-12T06:02:14.000Z",
"created_at_ist": "2026-10-12T11:32:14.000+05:30",
"member_answered_at": "2026-10-12T06:02:20.418Z",
"member_answered_at_ist": "2026-10-12T11:32:20.418+05:30",
"customer_answered_at": "2026-10-12T06:02:31.612Z",
"customer_answered_at_ist": "2026-10-12T11:32:31.612+05:30",
"ended_at": "2026-10-12T06:03:32.000Z",
"ended_at_ist": "2026-10-12T11:33:32.000+05:30",
"duration_sec": 61,
"outcome": "transferred",
"ended_by": "api",
"end_reason": "transferred",
"transferred_to": {
"kind": "member",
"member_id": 1042,
"member_name": "Neha Sharma",
"menu_id": null,
"menu_name": null,
"number": null
},
"recording_available": false,
"recording_url": null,
"disposition": null,
"sequence": 5,
"updated_at": "2026-10-12T06:03:32.000Z",
"updated_at_ist": "2026-10-12T11:33:32.000+05:30",
"dial_started_at": null,
"dial_started_at_ist": null,
"customer_ringing_at": "2026-10-12T06:02:20.418Z",
"customer_ringing_at_ist": "2026-10-12T11:32:20.418+05:30",
"member_ring_sec": 6,
"customer_ring_sec": 11,
"call_duration_sec": 248,
"recording": {
"status": "processing",
"revision": 0,
"url": null,
"format": null,
"size_bytes": null,
"duration_sec": null,
"channels": null,
"channel_layout": null,
"retention_days": 60,
"expires_at": "2026-12-11T06:02:14.000Z",
"parts": [],
"parts_ordered": false
}
}
],
"pagination": {
"page": 1,
"per_page": 50,
"total": 2,
"total_pages": 1
}
}Errors
| Status | Code | error_code | When | What to do |
|---|---|---|---|---|
| 400 | 400 | validation_error | A field is missing, has the wrong type or breaks a rule, or the body carries a field the API does not take. field names it, for example agent_id in "Unknown field: agent_id". | Fix the request, then send it with a new Idempotency-Key (a 400 is kept for the key). |
| 403 | 403 | feature_disabled | Your account does not have the Telephony API, or (on a transfer) the kind of transfer you asked for: transfer to a call menu, or to an outside number. | Contact your account manager. |
| 429 | 429 | rate_limited | Your request budget, the reading budget of a live click to call, or your account's calls a minute, is spent. | Wait for Retry-After. |
{
"status": "success",
"code": 200,
"data": [
{
"id": "c2c_9f1c4e7a2b6d48e0a3c5f1d29b7e6a40",
"object": "click_to_call",
"state": "completed",
"active": false,
"on_hold": false,
"mode": "internet",
"member": {
"id": 1042,
"name": "Neha Sharma"
},
"from_number": "+9111XXXXXX45",
"to_number": "+9198XXXXXX21",
"customer_name": "Ravi Kumar",
"call_id": "6b1e2f3a-4c5d-4e6f-8a7b-9c0d1e2f3a4b",
"reference_id": "LEAD-2291",
"metadata": {
"deal": "D-77",
"crm_owner": "neha"
},
"created_at": "2026-10-12T05:31:02.000Z",
"created_at_ist": "2026-10-12T11:01:02.000+05:30",
"member_answered_at": "2026-10-12T05:31:09.204Z",
"member_answered_at_ist": "2026-10-12T11:01:09.204+05:30",
"customer_answered_at": "2026-10-12T05:31:19.516Z",
"customer_answered_at_ist": "2026-10-12T11:01:19.516+05:30",
"ended_at": "2026-10-12T05:32:53.000Z",
"ended_at_ist": "2026-10-12T11:02:53.000+05:30",
"duration_sec": 94,
"outcome": "completed",
"ended_by": "customer",
"end_reason": "customer_ended",
"transferred_to": null,
"recording_available": true,
"recording_url": "https://voice.agentive.co.in/api/public/v1/calls/6b1e2f3a-4c5d-4e6f-8a7b-9c0d1e2f3a4b/recording",
"disposition": {
"outcome": "callback",
"note": "Wants the Gold plan quote on email first.",
"callback_at": "2026-10-14T05:30:00.000Z",
"tagged_at": "2026-10-12T05:34:05.000Z"
},
"sequence": 8,
"updated_at": "2026-10-12T05:34:05.000Z",
"updated_at_ist": "2026-10-12T11:04:05.000+05:30",
"dial_started_at": "2026-10-12T05:31:10.000Z",
"dial_started_at_ist": "2026-10-12T11:01:10.000+05:30",
"customer_ringing_at": "2026-10-12T05:31:10.400Z",
"customer_ringing_at_ist": "2026-10-12T11:01:10.400+05:30",
"member_ring_sec": 7,
"customer_ring_sec": 9,
"call_duration_sec": 94,
"recording": {
"status": "available",
"revision": 1,
"url": "https://voice.agentive.co.in/api/public/v1/calls/6b1e2f3a-4c5d-4e6f-8a7b-9c0d1e2f3a4b/recording",
"format": "ogg",
"size_bytes": 381952,
"duration_sec": 94.6,
"channels": 2,
"channel_layout": {
"left": "customer",
"right": "member"
},
"retention_days": 60,
"expires_at": "2026-12-11T05:31:10.000Z",
"parts": [
{
"part": 1,
"primary": true,
"segment": null,
"url": "https://voice.agentive.co.in/api/public/v1/calls/6b1e2f3a-4c5d-4e6f-8a7b-9c0d1e2f3a4b/recording",
"format": "ogg",
"size_bytes": 381952,
"duration_sec": 94.6,
"channels": 2,
"started_at": "2026-10-12T05:31:19.516Z",
"member": null
}
],
"parts_ordered": true
}
},
{
"id": "c2c_4d7b1e9c3a5f42d8b6e0c2a1f9d37b58",
"object": "click_to_call",
"state": "transferred",
"active": false,
"on_hold": false,
"mode": "phone_bridge",
"member": {
"id": 1057,
"name": "Arjun Mehta"
},
"from_number": "+9179XXXXXX12",
"to_number": "+9197XXXXXX08",
"customer_name": "Meera Iyer",
"call_id": "8d3f4b5c-6e7a-4b8c-9d0e-1f2a3b4c5d6e",
"reference_id": "LEAD-2304",
"metadata": {
"deal": "D-81"
},
"created_at": "2026-10-12T06:02:14.000Z",
"created_at_ist": "2026-10-12T11:32:14.000+05:30",
"member_answered_at": "2026-10-12T06:02:20.418Z",
"member_answered_at_ist": "2026-10-12T11:32:20.418+05:30",
"customer_answered_at": "2026-10-12T06:02:31.612Z",
"customer_answered_at_ist": "2026-10-12T11:32:31.612+05:30",
"ended_at": "2026-10-12T06:03:32.000Z",
"ended_at_ist": "2026-10-12T11:33:32.000+05:30",
"duration_sec": 61,
"outcome": "transferred",
"ended_by": "api",
"end_reason": "transferred",
"transferred_to": {
"kind": "member",
"member_id": 1042,
"member_name": "Neha Sharma",
"menu_id": null,
"menu_name": null,
"number": null
},
"recording_available": false,
"recording_url": null,
"disposition": null,
"sequence": 5,
"updated_at": "2026-10-12T06:03:32.000Z",
"updated_at_ist": "2026-10-12T11:33:32.000+05:30",
"dial_started_at": null,
"dial_started_at_ist": null,
"customer_ringing_at": "2026-10-12T06:02:20.418Z",
"customer_ringing_at_ist": "2026-10-12T11:32:20.418+05:30",
"member_ring_sec": 6,
"customer_ring_sec": 11,
"call_duration_sec": 248,
"recording": {
"status": "processing",
"revision": 0,
"url": null,
"format": null,
"size_bytes": null,
"duration_sec": null,
"channels": null,
"channel_layout": null,
"retention_days": 60,
"expires_at": "2026-12-11T06:02:14.000Z",
"parts": [],
"parts_ordered": false
}
}
],
"pagination": {
"page": null,
"per_page": 2,
"total": null,
"total_pages": null,
"next_cursor": "eyJrIjoiMTc5MTc4MjQ0NTAwMCJ9.c2Kp7w"
}
}Hang up
/click-to-call/{id}/hangupBefore the customer answers, this cancels the click to call (canceled): a team member still being rung stops ringing, and a customer still being called is not connected. After the customer answers, it ends the conversation (completed). Either way ended_by is api. A click to call that has already ended answers 200, unchanged (it keeps the side that ended it, even when that was a moment before your request), so it is safe to send twice.
Headers
| Header | Type | Required | Description |
|---|---|---|---|
| x-api-key | string | Required | Your publishable key ( pk_live_...). Treat it as confidential. See Authentication. |
| x-api-secret | string | Required | Your secret key ( sk_live_...). Server-side only. |
| Idempotency-Key | string | Optional | A key unique to this request, 1 to 128 visible ASCII characters. Send one on every POST: a retry with the same key returns the first answer and never rings anyone twice. See Idempotency.Example: lead-2291-call-1 |
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| id | string | Required | The click to call id, c2c_ and 32 hex characters, as returned in unique_id when you started it. Another account's id answers 404.Example: c2c_9f1c4e7a2b6d48e0a3c5f1d29b7e6a40 |
Request
curl -X POST https://voice.agentive.co.in/api/public/v1/click-to-call/c2c_7a2e5c9d1b3f46a8c0e4d2b6f8a1c3e5/hangup \
-H "x-api-key: $AGENTIVE_KEY" \
-H "x-api-secret: $AGENTIVE_SECRET"Response (hung up while the team member was ringing)
{
"status": "success",
"code": 200,
"unique_id": "c2c_7a2e5c9d1b3f46a8c0e4d2b6f8a1c3e5",
"reference_id": "LEAD-2310",
"details": "canceled",
"click_to_call": {
"id": "c2c_7a2e5c9d1b3f46a8c0e4d2b6f8a1c3e5",
"object": "click_to_call",
"state": "canceled",
"active": false,
"on_hold": false,
"mode": "internet",
"member": {
"id": 1042,
"name": "Neha Sharma"
},
"from_number": "+9111XXXXXX45",
"to_number": "+9198XXXXXX21",
"customer_name": null,
"call_id": null,
"reference_id": "LEAD-2310",
"metadata": null,
"created_at": "2026-10-12T06:40:00.000Z",
"created_at_ist": "2026-10-12T12:10:00.000+05:30",
"member_answered_at": null,
"member_answered_at_ist": null,
"customer_answered_at": null,
"customer_answered_at_ist": null,
"ended_at": "2026-10-12T06:40:07.000Z",
"ended_at_ist": "2026-10-12T12:10:07.000+05:30",
"duration_sec": 0,
"outcome": "canceled",
"ended_by": "api",
"end_reason": "cancelled",
"transferred_to": null,
"recording_available": false,
"recording_url": null,
"disposition": null,
"sequence": 2,
"updated_at": "2026-10-12T06:40:07.000Z",
"updated_at_ist": "2026-10-12T12:10:07.000+05:30",
"dial_started_at": null,
"dial_started_at_ist": null,
"customer_ringing_at": null,
"customer_ringing_at_ist": null,
"member_ring_sec": 7,
"customer_ring_sec": null,
"call_duration_sec": null,
"recording": {
"status": "none",
"revision": 0,
"url": null,
"format": null,
"size_bytes": null,
"duration_sec": null,
"channels": null,
"channel_layout": null,
"retention_days": 60,
"expires_at": "2026-12-11T06:40:00.000Z",
"parts": [],
"parts_ordered": false
}
}
}Errors
| Status | Code | error_code | When | What to do |
|---|---|---|---|---|
| 404 | 404 | not_found | No click to call with that id on your account. | Check the id. |
| 503 | 503 | service_unavailable | The line for this caller ID cannot be reached right now, the call could not be reached to carry out a control, or the record could not be read. No Retry-After is sent. When the body says retryable: false (details "Click to call is switched off for this account."), click to call is not switched on for your account right now: every route answers this, before the Telephony API is checked. | Wait about 30 seconds (add some jitter), then try again. After a keypad or transfer request we cannot be sure nothing was done: read the click to call first and use a new Idempotency-Key. If retryable is false, do not retry: contact your account manager. |
Over the internet, a hang up before the customer is called reaches its outcome at once. Once a call is on the line, the answer comes back at once with the call as it stands (still active, ended_by api), and the outcome and click_to_call.ended follow about five seconds after the call ends, like call.completed. Until then a new click to call for the same team member answers member_on_call (retryable): try again a few seconds later.
Send keypad tones
/click-to-call/{id}/dtmfPlay keypad tones into the call, in order, for example to pick an option in a phone menu the customer's side runs. Only while in_progress and not on hold. Send an Idempotency-Key: a blind retry without one would press the keys twice. The digits are never stored in call records, events or API logs.
Headers
| Header | Type | Required | Description |
|---|---|---|---|
| x-api-key | string | Required | Your publishable key ( pk_live_...). Treat it as confidential. See Authentication. |
| x-api-secret | string | Required | Your secret key ( sk_live_...). Server-side only. |
| Content-Type | string | Required | Always application/json. |
| Idempotency-Key | string | Optional | A key unique to this request, 1 to 128 visible ASCII characters. Send one on every POST: a retry with the same key returns the first answer and never rings anyone twice. See Idempotency.Example: lead-2291-call-1 |
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| id | string | Required | The click to call id, c2c_ and 32 hex characters, as returned in unique_id when you started it. Another account's id answers 404.Example: c2c_9f1c4e7a2b6d48e0a3c5f1d29b7e6a40 |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| digits | string | Required | 1 to 32 of 0 to 9, *, # and A to D.Example: 1234# |
Request
curl -X POST https://voice.agentive.co.in/api/public/v1/click-to-call/c2c_9f1c4e7a2b6d48e0a3c5f1d29b7e6a40/dtmf \
-H "x-api-key: $AGENTIVE_KEY" \
-H "x-api-secret: $AGENTIVE_SECRET" \
-H "Idempotency-Key: lead-2291-dtmf-1" \
-H "Content-Type: application/json" \
-d '{ "digits": "1234#" }'Response
{
"status": "success",
"code": 200,
"unique_id": "c2c_9f1c4e7a2b6d48e0a3c5f1d29b7e6a40",
"reference_id": "LEAD-2291",
"details": "in_progress",
"click_to_call": {
"id": "c2c_9f1c4e7a2b6d48e0a3c5f1d29b7e6a40",
"object": "click_to_call",
"state": "in_progress",
"active": true,
"on_hold": false,
"mode": "internet",
"member": {
"id": 1042,
"name": "Neha Sharma"
},
"from_number": "+9111XXXXXX45",
"to_number": "+9198XXXXXX21",
"customer_name": "Ravi Kumar",
"call_id": "6b1e2f3a-4c5d-4e6f-8a7b-9c0d1e2f3a4b",
"reference_id": "LEAD-2291",
"metadata": {
"deal": "D-77",
"crm_owner": "neha"
},
"created_at": "2026-10-12T05:31:02.000Z",
"created_at_ist": "2026-10-12T11:01:02.000+05:30",
"member_answered_at": "2026-10-12T05:31:09.204Z",
"member_answered_at_ist": "2026-10-12T11:01:09.204+05:30",
"customer_answered_at": "2026-10-12T05:31:19.516Z",
"customer_answered_at_ist": "2026-10-12T11:01:19.516+05:30",
"ended_at": null,
"ended_at_ist": null,
"duration_sec": 42,
"outcome": null,
"ended_by": null,
"end_reason": null,
"transferred_to": null,
"recording_available": false,
"recording_url": null,
"disposition": null,
"sequence": 4,
"updated_at": "2026-10-12T05:31:19.516Z",
"updated_at_ist": "2026-10-12T11:01:19.516+05:30",
"dial_started_at": "2026-10-12T05:31:10.000Z",
"dial_started_at_ist": "2026-10-12T11:01:10.000+05:30",
"customer_ringing_at": "2026-10-12T05:31:10.400Z",
"customer_ringing_at_ist": "2026-10-12T11:01:10.400+05:30",
"member_ring_sec": 7,
"customer_ring_sec": 9,
"call_duration_sec": null,
"recording": {
"status": "processing",
"revision": 0,
"url": null,
"format": null,
"size_bytes": null,
"duration_sec": null,
"channels": null,
"channel_layout": null,
"retention_days": 60,
"expires_at": "2026-12-11T05:31:10.000Z",
"parts": [],
"parts_ordered": false
}
}
}Errors
| Status | Code | error_code | When | What to do |
|---|---|---|---|---|
| 400 | 400 | validation_error | A field is missing, has the wrong type or breaks a rule, or the body carries a field the API does not take. field names it, for example agent_id in "Unknown field: agent_id". | Fix the request, then send it with a new Idempotency-Key (a 400 is kept for the key). |
| 409 | 409 | call_not_connected | Keypad, hold or transfer was sent before the customer answered. No leg was touched. | Wait for click_to_call.customer_answered, then send it again. |
| 409 | 409 | call_on_hold | Keypad tones were sent while the call is on hold through this API (a hold the team member made from their panel or app is not known here). | Resume the call first. |
| 410 | 410 | call_ended | The click to call has ended. | Nothing to do. Hang up on an ended call answers 200 instead. |
| 410 | 410 | call_transferred | The click to call was transferred: the customer is now with a colleague, a call menu or another number. | Follow the rest of the call through the account's call events. |
| 404 | 404 | not_found | No click to call with that id on your account. | Check the id. |
| 503 | 503 | service_unavailable | The line for this caller ID cannot be reached right now, the call could not be reached to carry out a control, or the record could not be read. No Retry-After is sent. When the body says retryable: false (details "Click to call is switched off for this account."), click to call is not switched on for your account right now: every route answers this, before the Telephony API is checked. | Wait about 30 seconds (add some jitter), then try again. After a keypad or transfer request we cannot be sure nothing was done: read the click to call first and use a new Idempotency-Key. If retryable is false, do not retry: contact your account manager. |
Hold
/click-to-call/{id}/holdPut the customer on hold: they hear hold music, and the team member's web panel shows the call as Held. Only while in_progress. Holding a call already on hold answers 200 and sends no second event.
Headers
| Header | Type | Required | Description |
|---|---|---|---|
| x-api-key | string | Required | Your publishable key ( pk_live_...). Treat it as confidential. See Authentication. |
| x-api-secret | string | Required | Your secret key ( sk_live_...). Server-side only. |
| Idempotency-Key | string | Optional | A key unique to this request, 1 to 128 visible ASCII characters. Send one on every POST: a retry with the same key returns the first answer and never rings anyone twice. See Idempotency.Example: lead-2291-call-1 |
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| id | string | Required | The click to call id, c2c_ and 32 hex characters, as returned in unique_id when you started it. Another account's id answers 404.Example: c2c_9f1c4e7a2b6d48e0a3c5f1d29b7e6a40 |
Request
curl -X POST https://voice.agentive.co.in/api/public/v1/click-to-call/c2c_9f1c4e7a2b6d48e0a3c5f1d29b7e6a40/hold \
-H "x-api-key: $AGENTIVE_KEY" \
-H "x-api-secret: $AGENTIVE_SECRET"Response
{
"status": "success",
"code": 200,
"unique_id": "c2c_9f1c4e7a2b6d48e0a3c5f1d29b7e6a40",
"reference_id": "LEAD-2291",
"details": "in_progress",
"click_to_call": {
"id": "c2c_9f1c4e7a2b6d48e0a3c5f1d29b7e6a40",
"object": "click_to_call",
"state": "in_progress",
"active": true,
"on_hold": true,
"mode": "internet",
"member": {
"id": 1042,
"name": "Neha Sharma"
},
"from_number": "+9111XXXXXX45",
"to_number": "+9198XXXXXX21",
"customer_name": "Ravi Kumar",
"call_id": "6b1e2f3a-4c5d-4e6f-8a7b-9c0d1e2f3a4b",
"reference_id": "LEAD-2291",
"metadata": {
"deal": "D-77",
"crm_owner": "neha"
},
"created_at": "2026-10-12T05:31:02.000Z",
"created_at_ist": "2026-10-12T11:01:02.000+05:30",
"member_answered_at": "2026-10-12T05:31:09.204Z",
"member_answered_at_ist": "2026-10-12T11:01:09.204+05:30",
"customer_answered_at": "2026-10-12T05:31:19.516Z",
"customer_answered_at_ist": "2026-10-12T11:01:19.516+05:30",
"ended_at": null,
"ended_at_ist": null,
"duration_sec": 51,
"outcome": null,
"ended_by": null,
"end_reason": null,
"transferred_to": null,
"recording_available": false,
"recording_url": null,
"disposition": null,
"sequence": 5,
"updated_at": "2026-10-12T05:31:19.516Z",
"updated_at_ist": "2026-10-12T11:01:19.516+05:30",
"dial_started_at": "2026-10-12T05:31:10.000Z",
"dial_started_at_ist": "2026-10-12T11:01:10.000+05:30",
"customer_ringing_at": "2026-10-12T05:31:10.400Z",
"customer_ringing_at_ist": "2026-10-12T11:01:10.400+05:30",
"member_ring_sec": 7,
"customer_ring_sec": 9,
"call_duration_sec": null,
"recording": {
"status": "processing",
"revision": 0,
"url": null,
"format": null,
"size_bytes": null,
"duration_sec": null,
"channels": null,
"channel_layout": null,
"retention_days": 60,
"expires_at": "2026-12-11T05:31:10.000Z",
"parts": [],
"parts_ordered": false
}
}
}Errors
| Status | Code | error_code | When | What to do |
|---|---|---|---|---|
| 409 | 409 | call_not_connected | Keypad, hold or transfer was sent before the customer answered. No leg was touched. | Wait for click_to_call.customer_answered, then send it again. |
| 410 | 410 | call_ended | The click to call has ended. | Nothing to do. Hang up on an ended call answers 200 instead. |
| 410 | 410 | call_transferred | The click to call was transferred: the customer is now with a colleague, a call menu or another number. | Follow the rest of the call through the account's call events. |
| 404 | 404 | not_found | No click to call with that id on your account. | Check the id. |
| 503 | 503 | service_unavailable | The line for this caller ID cannot be reached right now, the call could not be reached to carry out a control, or the record could not be read. No Retry-After is sent. When the body says retryable: false (details "Click to call is switched off for this account."), click to call is not switched on for your account right now: every route answers this, before the Telephony API is checked. | Wait about 30 seconds (add some jitter), then try again. After a keypad or transfer request we cannot be sure nothing was done: read the click to call first and use a new Idempotency-Key. If retryable is false, do not retry: contact your account manager. |
{
"status": "error",
"code": 409,
"unique_id": null,
"reference_id": "LEAD-2291",
"details": "The customer has not answered yet.",
"error_code": "call_not_connected",
"error": "The customer has not answered yet.",
"retryable": true
}Resume
/click-to-call/{id}/unholdTake the customer off hold. Resuming a call that is not on hold answers 200 and sends no event. When the team member takes a hold you made through the API off from their own panel, you receive click_to_call.resumed just the same. A hold the team member makes from their panel or app is not reported.
Headers
| Header | Type | Required | Description |
|---|---|---|---|
| x-api-key | string | Required | Your publishable key ( pk_live_...). Treat it as confidential. See Authentication. |
| x-api-secret | string | Required | Your secret key ( sk_live_...). Server-side only. |
| Idempotency-Key | string | Optional | A key unique to this request, 1 to 128 visible ASCII characters. Send one on every POST: a retry with the same key returns the first answer and never rings anyone twice. See Idempotency.Example: lead-2291-call-1 |
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| id | string | Required | The click to call id, c2c_ and 32 hex characters, as returned in unique_id when you started it. Another account's id answers 404.Example: c2c_9f1c4e7a2b6d48e0a3c5f1d29b7e6a40 |
Request
curl -X POST https://voice.agentive.co.in/api/public/v1/click-to-call/c2c_9f1c4e7a2b6d48e0a3c5f1d29b7e6a40/unhold \
-H "x-api-key: $AGENTIVE_KEY" \
-H "x-api-secret: $AGENTIVE_SECRET"Response
{
"status": "success",
"code": 200,
"unique_id": "c2c_9f1c4e7a2b6d48e0a3c5f1d29b7e6a40",
"reference_id": "LEAD-2291",
"details": "in_progress",
"click_to_call": {
"id": "c2c_9f1c4e7a2b6d48e0a3c5f1d29b7e6a40",
"object": "click_to_call",
"state": "in_progress",
"active": true,
"on_hold": false,
"mode": "internet",
"member": {
"id": 1042,
"name": "Neha Sharma"
},
"from_number": "+9111XXXXXX45",
"to_number": "+9198XXXXXX21",
"customer_name": "Ravi Kumar",
"call_id": "6b1e2f3a-4c5d-4e6f-8a7b-9c0d1e2f3a4b",
"reference_id": "LEAD-2291",
"metadata": {
"deal": "D-77",
"crm_owner": "neha"
},
"created_at": "2026-10-12T05:31:02.000Z",
"created_at_ist": "2026-10-12T11:01:02.000+05:30",
"member_answered_at": "2026-10-12T05:31:09.204Z",
"member_answered_at_ist": "2026-10-12T11:01:09.204+05:30",
"customer_answered_at": "2026-10-12T05:31:19.516Z",
"customer_answered_at_ist": "2026-10-12T11:01:19.516+05:30",
"ended_at": null,
"ended_at_ist": null,
"duration_sec": 81,
"outcome": null,
"ended_by": null,
"end_reason": null,
"transferred_to": null,
"recording_available": false,
"recording_url": null,
"disposition": null,
"sequence": 6,
"updated_at": "2026-10-12T05:31:19.516Z",
"updated_at_ist": "2026-10-12T11:01:19.516+05:30",
"dial_started_at": "2026-10-12T05:31:10.000Z",
"dial_started_at_ist": "2026-10-12T11:01:10.000+05:30",
"customer_ringing_at": "2026-10-12T05:31:10.400Z",
"customer_ringing_at_ist": "2026-10-12T11:01:10.400+05:30",
"member_ring_sec": 7,
"customer_ring_sec": 9,
"call_duration_sec": null,
"recording": {
"status": "processing",
"revision": 0,
"url": null,
"format": null,
"size_bytes": null,
"duration_sec": null,
"channels": null,
"channel_layout": null,
"retention_days": 60,
"expires_at": "2026-12-11T05:31:10.000Z",
"parts": [],
"parts_ordered": false
}
}
}Errors
| Status | Code | error_code | When | What to do |
|---|---|---|---|---|
| 409 | 409 | call_not_connected | Keypad, hold or transfer was sent before the customer answered. No leg was touched. | Wait for click_to_call.customer_answered, then send it again. |
| 410 | 410 | call_ended | The click to call has ended. | Nothing to do. Hang up on an ended call answers 200 instead. |
| 410 | 410 | call_transferred | The click to call was transferred: the customer is now with a colleague, a call menu or another number. | Follow the rest of the call through the account's call events. |
| 404 | 404 | not_found | No click to call with that id on your account. | Check the id. |
| 503 | 503 | service_unavailable | The line for this caller ID cannot be reached right now, the call could not be reached to carry out a control, or the record could not be read. No Retry-After is sent. When the body says retryable: false (details "Click to call is switched off for this account."), click to call is not switched on for your account right now: every route answers this, before the Telephony API is checked. | Wait about 30 seconds (add some jitter), then try again. After a keypad or transfer request we cannot be sure nothing was done: read the click to call first and use a new Idempotency-Key. If retryable is false, do not retry: contact your account manager. |
Transfer
/click-to-call/{id}/transferHand the customer on, while in_progress: to a colleague (rung on their own lane, like any transfer), to a call menu, or to an outside number. The click to call then ends as transferred, with ended_by api and transferred_to naming the target; the rest of the customer's call is reported by your account's normal call events. The call itself goes on: once a colleague answers it, the call's record names that colleague as its member, and its ended_by tells how the call then ended (null when that is not known). On a move to a call menu its recording ends at the move; the call menu part is a call of its own, lane menu.
Headers
| Header | Type | Required | Description |
|---|---|---|---|
| x-api-key | string | Required | Your publishable key ( pk_live_...). Treat it as confidential. See Authentication. |
| x-api-secret | string | Required | Your secret key ( sk_live_...). Server-side only. |
| Content-Type | string | Required | Always application/json. |
| Idempotency-Key | string | Optional | A key unique to this request, 1 to 128 visible ASCII characters. Send one on every POST: a retry with the same key returns the first answer and never rings anyone twice. See Idempotency.Example: lead-2291-call-1 |
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| id | string | Required | The click to call id, c2c_ and 32 hex characters, as returned in unique_id when you started it. Another account's id answers 404.Example: c2c_9f1c4e7a2b6d48e0a3c5f1d29b7e6a40 |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| to_member_id | number | Optional | A colleague, by id. Send exactly one of the four. Example: 1042 |
| to_member_email | string | Optional | A colleague, by email. Example: neha@example.in |
| to_menu_id | number | Optional | A call menu from List call menus. Needs transfer to a call menu on your account. Example: 12 |
| to_number | string | Optional | An outside Indian number. Needs call forwarding to outside numbers on your account, and every check of Start a click to call applies to it, the per-number limits included. Example: 98XXXXXX34 |
Request
curl -X POST https://voice.agentive.co.in/api/public/v1/click-to-call/c2c_4d7b1e9c3a5f42d8b6e0c2a1f9d37b58/transfer \
-H "x-api-key: $AGENTIVE_KEY" \
-H "x-api-secret: $AGENTIVE_SECRET" \
-H "Idempotency-Key: lead-2304-transfer-1" \
-H "Content-Type: application/json" \
-d '{ "to_member_email": "neha@example.in" }'Response
{
"status": "success",
"code": 200,
"unique_id": "c2c_4d7b1e9c3a5f42d8b6e0c2a1f9d37b58",
"reference_id": "LEAD-2304",
"details": "transferred",
"click_to_call": {
"id": "c2c_4d7b1e9c3a5f42d8b6e0c2a1f9d37b58",
"object": "click_to_call",
"state": "transferred",
"active": false,
"on_hold": false,
"mode": "phone_bridge",
"member": {
"id": 1057,
"name": "Arjun Mehta"
},
"from_number": "+9179XXXXXX12",
"to_number": "+9197XXXXXX08",
"customer_name": "Meera Iyer",
"call_id": "8d3f4b5c-6e7a-4b8c-9d0e-1f2a3b4c5d6e",
"reference_id": "LEAD-2304",
"metadata": {
"deal": "D-81"
},
"created_at": "2026-10-12T06:02:14.000Z",
"created_at_ist": "2026-10-12T11:32:14.000+05:30",
"member_answered_at": "2026-10-12T06:02:20.418Z",
"member_answered_at_ist": "2026-10-12T11:32:20.418+05:30",
"customer_answered_at": "2026-10-12T06:02:31.612Z",
"customer_answered_at_ist": "2026-10-12T11:32:31.612+05:30",
"ended_at": "2026-10-12T06:03:32.000Z",
"ended_at_ist": "2026-10-12T11:33:32.000+05:30",
"duration_sec": 61,
"outcome": "transferred",
"ended_by": "api",
"end_reason": "transferred",
"transferred_to": {
"kind": "member",
"member_id": 1042,
"member_name": "Neha Sharma",
"menu_id": null,
"menu_name": null,
"number": null
},
"recording_available": false,
"recording_url": null,
"disposition": null,
"sequence": 5,
"updated_at": "2026-10-12T06:03:32.000Z",
"updated_at_ist": "2026-10-12T11:33:32.000+05:30",
"dial_started_at": null,
"dial_started_at_ist": null,
"customer_ringing_at": "2026-10-12T06:02:20.418Z",
"customer_ringing_at_ist": "2026-10-12T11:32:20.418+05:30",
"member_ring_sec": 6,
"customer_ring_sec": 11,
"call_duration_sec": 248,
"recording": {
"status": "processing",
"revision": 0,
"url": null,
"format": null,
"size_bytes": null,
"duration_sec": null,
"channels": null,
"channel_layout": null,
"retention_days": 60,
"expires_at": "2026-12-11T06:02:14.000Z",
"parts": [],
"parts_ordered": false
}
}
}Errors
| Status | Code | error_code | When | What to do |
|---|---|---|---|---|
| 400 | 400 | validation_error | A field is missing, has the wrong type or breaks a rule, or the body carries a field the API does not take. field names it, for example agent_id in "Unknown field: agent_id". | Fix the request, then send it with a new Idempotency-Key (a 400 is kept for the key). |
| 409 | 409 | call_not_connected | Keypad, hold or transfer was sent before the customer answered. No leg was touched. | Wait for click_to_call.customer_answered, then send it again. |
| 410 | 410 | call_ended | The click to call has ended. | Nothing to do. Hang up on an ended call answers 200 instead. |
| 410 | 410 | call_transferred | The click to call was transferred: the customer is now with a colleague, a call menu or another number. | Follow the rest of the call through the account's call events. |
| 404 | 404 | member_not_found | No team member with that id or email on your account. Also answered for a colleague named on a transfer. | Check the id or email with List team members. |
| 404 | 404 | menu_not_found | No call menu with that id on your account. | Check the id with List call menus. |
| 409 | 409 | target_on_call | The colleague you are transferring to is on another call. | Try again in a moment, or pick someone else. |
| 409 | 409 | target_unavailable | The colleague is Offline, on a break, or cannot be rung right now. | Pick someone else, or a call menu. |
| 403 | 403 | feature_disabled | Your account does not have the Telephony API, or (on a transfer) the kind of transfer you asked for: transfer to a call menu, or to an outside number. | Contact your account manager. |
| 400 | 400 | invalid_number | to (or to_number on a transfer) is not a number we can dial in India, or it is one of your own account's numbers. | Fix the number. |
| 400 | 400 | international_not_supported | The number is outside India, or your account's filter for numbers outside India refuses it. | Call only Indian numbers. |
| 422 | 422 | number_blocked | The number is on the do not disturb list, or on your account's or the team member's block list. | Do not call this number. |
| 429 | 429 | number_flood | 3 calls through the API to this number in 10 minutes, or 10 today (IST), counted across every API that places calls. Both limits answer this one code; details says which. | Wait for Retry-After (up to 600 seconds, or until midnight IST). Do not retry sooner. |
| 404 | 404 | not_found | No click to call with that id on your account. | Check the id. |
| 503 | 503 | service_unavailable | The line for this caller ID cannot be reached right now, the call could not be reached to carry out a control, or the record could not be read. No Retry-After is sent. When the body says retryable: false (details "Click to call is switched off for this account."), click to call is not switched on for your account right now: every route answers this, before the Telephony API is checked. | Wait about 30 seconds (add some jitter), then try again. After a keypad or transfer request we cannot be sure nothing was done: read the click to call first and use a new Idempotency-Key. If retryable is false, do not retry: contact your account manager. |
{ "to_member_id": 1042 }
{ "to_member_email": "neha@example.in" }
{ "to_menu_id": 12 }
{ "to_number": "98XXXXXX34" }Download the recording
/calls/{call_id}/recordingThe call's audio, once recording_available is true (its recording_url is this address). The path takes the call_id or the click to call id itself (c2c_...); add ?segment=N for an earlier part listed in recording.parts. 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, never a redirect. A Range header gets HTTP 206; a range it cannot serve, HTTP 416 (a range past the end carries Content-Range: bytes */<file size>). Needs call recording on your account. GET /calls/{unique_id} also accepts the c2c_ id and answers the call record, as described in the Voice Broadcast API.
Headers
| Header | Type | Required | Description |
|---|---|---|---|
| x-api-key | string | Required | Your publishable key ( pk_live_...). Treat it as confidential. See Authentication. |
| x-api-secret | string | Required | Your secret key ( sk_live_...). Server-side only. |
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| call_id | string | Required | The click to call's call_id, or its c2c_ id. Example: 6b1e2f3a-4c5d-4e6f-8a7b-9c0d1e2f3a4b |
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/6b1e2f3a-4c5d-4e6f-8a7b-9c0d1e2f3a4b/recording" \
-u "$AGENTIVE_KEY:$AGENTIVE_SECRET" \
--output call.oggErrors
| Status | Code | error_code | When | What to do |
|---|---|---|---|---|
| 404 | 404 | not_found | No recording exists for the call, or not yet (details "No recording is available for this call."), or no part has that segment ("Segment not found."). | Wait for recording.available, or for recording_available to be true. |
| 404 | 404 | recording_expired | The recording is past its recording.expires_at (your plan's retention) and is no longer available. | Copy recordings before expires_at. |
| 403 | 403 | feature_disabled | Call recording is not enabled for your account. | |
| 416 | 416 | validation_error | The Range header is malformed, names several ranges, or starts past the end of the file. A range past the end carries Content-Range: bytes */<file size> (since 14 October 2026) whenever we know the file's size. The body's unique_id names the call: the same unique_id that GET /calls/{id} answers for the id in your path (since 14 October 2026; before, null). Every other error here keeps unique_id null. | |
| 502 | 502 | upstream_error | The audio could not be fetched just now. | Retry after a short pause. |
x-api-key and x-api-secret instead, switch off redirect following for this request.The click to call object
Every answer on this page and every click_to_call.* event carries the same object. Phone numbers are full E.164 (+91 and ten digits). Every time comes twice: in UTC with a Z, and the same instant at +05:30 in the _ist twin.
Fields
| Field | Type | Description |
|---|---|---|
| id | string | c2c_ and 32 hex characters. The unique_id of every answer and event for this call. |
| object | string | Always click_to_call. |
| state | string | |
| active | boolean | true while the call is still going (one of the four live states). |
| on_hold | boolean | true while the customer is on hold through this API (Hold). A hold the team member makes from their own panel or app is not reflected here. If they take an API hold off from their panel, on_hold goes back to false and click_to_call.resumed is sent. |
| mode | string | internet or phone_bridge: how the team member was rung. |
| member | object | { id, name } of the team member. |
| from_number | string | The number the customer sees, in E.164. |
| to_number | string | The customer, in E.164. |
| customer_name | string | null | What you sent in customer_name, cleaned. |
| call_id | string | null | The call record behind it. Use it for the recording. Over the internet it is set once the customer is being called, and is null before that and on a call that never got that far. On Phone Bridge it is set from member_ringing, because the call record starts when the team member's mobile is rung. |
| reference_id | string | null | What you sent. |
| metadata | object | null | What you sent. Echoed, never spoken. A number comes back as text. |
| created_at | string | When you started it, ISO 8601 UTC. |
| created_at_ist | string | The same instant at +05:30. |
| member_answered_at | string | null | When the team member answered, ISO 8601 UTC with milliseconds. On Phone Bridge before 14 October 2026, to the second. |
| member_answered_at_ist | string | null | The same instant at +05:30. |
| customer_answered_at | string | null | When the customer answered, ISO 8601 UTC with milliseconds (before 14 October 2026, to the second). The call record's answered_at for the same call is to the second, so it can read up to one second earlier. |
| customer_answered_at_ist | string | null | The same instant at +05:30. |
| ended_at | string | null | When it ended (or was transferred), ISO 8601 UTC. null while active. Since 14 October 2026 it usually carries milliseconds: the moment our server recorded the end. It can still read to the second, for example when the end was recorded another way or after a restart of our service. Ends that do not come from the call record (over the internet, a cancel before the customer is called; a ring on the team member's web panel or app that runs out or that they decline; a call that could not be placed; a transfer you request) carried milliseconds before too. The call record's ended_at for the same call is to the second, so it can read up to one second earlier. A call that ends in the same second as customer_answered_at (or, on Phone Bridge, member_answered_at) never reads an end earlier than that answer. |
| ended_at_ist | string | null | The same instant at +05:30. |
| duration_sec | number | Your team member's talk time in seconds, counted from the customer's answer. 0 when the customer never answered. After a transfer it covers your team member's part only; the whole conversation is call_duration_sec. |
| outcome | string | null | How it ended; null while active. See Outcomes. Branch on this. |
| ended_by | string | null | |
| end_reason | string | null | A short code for your logs about why it ended: the call's reason (for example customer_ended, member_ended, busy, no_answer, transferred or cancelled) or, when the call could not be placed, the error code it was refused with. On a connected call that was not transferred it is null unless ended_by is member or customer. Branch on outcome, not on this. |
| transferred_to | object | null | Who the call was handed to: { "kind", "member_id", "member_name", "menu_id", "menu_name", "number" }. kind is member (a colleague, member_id and member_name), menu (a call menu, menu_id and menu_name) or number (an outside number, number in E.164). The keys that do not apply to the kind are null. Always set when you transferred the call through the API. When the team member transferred it from their panel or app, it names the colleague or the number when we know it (a call menu is not named), and is null otherwise. |
| recording_available | boolean | The recording is complete and can be downloaded now. It turns true about a minute after the last part is stored, together with recording.available. |
| recording_url | string | null | The authenticated GET /calls/{call_id}/recording address of the main file once the recording is ready, else null. It needs your keys. Earlier parts are in recording.parts. |
| disposition | object | null | Set when the team member saves an outcome for the call in the panel or app: { outcome, note, callback_at, tagged_at }. outcome is the code they picked, for example interested, callback or not_interested; treat one you do not recognise as other. As saved: it does not follow a callback later removed or moved to a newer reminder (the call's own disposition does). |
| sequence | number | Goes up with every change. Keep the copy with the highest sequence. |
| updated_at | string | When the click to call last changed, ISO 8601 UTC. Use the latest one you hold for your next catch-up. |
| updated_at_ist | string | The same instant at +05:30. |
| dial_started_at | string | null | When we started placing the customer's call over the internet. null until then, and on Phone Bridge (see customer_ringing_at). |
| dial_started_at_ist | string | null | The same instant at +05:30. |
| customer_ringing_at | string | null | When the customer's call was placed (the customer_ringing state). null until then. On Phone Bridge it is the moment the team member's mobile answered, because we call the customer then (since 14 October 2026; before, the moment we noticed that answer, up to a second later). null when the customer was never called: on Phone Bridge, a team member who did not answer (before 14 October 2026 such a click to call carried a time a few seconds after its end). |
| customer_ringing_at_ist | string | null | The same instant at +05:30. |
| member_ring_sec | number | null | How long your team member was rung: from the start to their answer, or to the end. Whole seconds, rounded down from the times above. null while they are still being rung. |
| customer_ring_sec | number | null | How long the customer was called: from customer_ringing_at to their answer, or to the end. Whole seconds, rounded down from the times above. null while the customer is still being called (and before), and when it is not known (some Phone Bridge click to calls). Since 14 October 2026 the answer times carry milliseconds, so a ring of 1.4 seconds reads 1: before, both ring lengths read 0 on most answered calls and on every Phone Bridge call. |
| call_duration_sec | number | null | The talk time of the whole call record, after a transfer included (the talk_sec of the call). null while the call is live and when the customer was never called. |
| recording | object | The call's recording block: status, every part with its own download address, the stereo layout and expires_at, as on the call object. status is none while there is no call record. |
States and outcomes
A click to call moves forward through up to four live states and ends in one outcome. It never moves backwards. Every state it passes through is reported once and in order, even one that went by very quickly.
| Live state | What it means | Event |
|---|---|---|
| member_ringing | Your team member is being rung. | click_to_call.member_ringing |
| member_answered | They answered. Over the internet we place the customer's call a moment later; on Phone Bridge at once. | click_to_call.member_answered |
| customer_ringing | We are calling the customer. Their phone may not have started ringing yet: the network does not tell us when it does. | click_to_call.customer_ringing |
| in_progress | The customer answered and they are talking. on_hold changes while it is in progress. | click_to_call.customer_answered, then .held and .resumed |
Outcomes
Once active is false, state and outcome hold the same value, one of these:
| outcome | What happened | Worth another try? |
|---|---|---|
| completed | They talked, and the call ended (whoever ended it). | Not needed. |
| transferred | The team member handed the customer to a colleague, a call menu or a number. | Not needed. |
| member_no_answer | Your team member did not answer in time. The customer was never called. | Later, or ring someone else. |
| member_declined | Your team member declined. The customer was never called. | Ask them, or ring someone else. |
| customer_no_answer | The customer's phone rang and nobody answered. | Later. |
| customer_busy | The customer's line was busy. | In a few minutes. |
| customer_rejected | The customer declined the call. | Not soon. |
| customer_unreachable | The customer's phone was switched off, out of coverage, or cannot take calls. | Later. |
| invalid_number | The number does not exist. | No: fix the number. |
| canceled | Ended before the customer answered: by your system's hang up, or by the team member. | If you meant to. |
| failed | The call could not be completed on the network or on our side. | Yes, in a minute. |
Who ended the call
ended_by is for your reports; branch on outcome.
| ended_by | Means |
|---|---|
| member | Your team member hung up (on their own mobile too, with Phone Bridge), or ended the call before the customer answered, or someone on your team (an admin or manager) ended it from Live Monitor. |
| customer | The customer hung up, or their side refused the call before it connected (busy or declined). |
| api | Your system, through Hang up or Transfer. |
| system | We ended it, for example when the line dropped, or our support team ended the call from Live Monitor. |
| null | Nobody hung up, or we could not tell which side did. For example: nobody answered, the customer's phone could not be reached, the call was transferred from the panel or app, or a connected call whose ending side was not reported (common when the call rings your team member's own mobile). A connected call can be completed with ended_by null: use outcome to know whether they talked. Since 15 October 2026 a Phone Bridge click to call your team member hung up on their own mobile reads member (end_reason member_ended), and a click to call someone on your team (an admin or manager) ended from Live Monitor reads member and one our support team ended reads system, as the call itself reads; before, these read null, or customer on a click to call over the internet. |
Webhooks
Rather than polling, let the call come to you. Click to call sends events to the account webhook endpoints you register under Developers > Webhooks in the Voice dashboard, where they have their own Click to call group. They are offered only to an account with the Telephony API. Each delivery is signed, retried for about 20 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. Send test event works for every one of them.
https:// address on port 443 or 8443, checked on every delivery. A custom signing secret must be at least 32 characters, and an account can register up to 10 endpoints. There is no per-call callback address: events go only to the endpoints registered on your account.The events
Every event carries the whole click to call object in data (data.object is click_to_call), as it was at that event's step, with that event's own sequence, and the envelope's reference_id is yours.
| Event | Sent when |
|---|---|
click_to_call.member_ringing | Your team member is being rung. |
click_to_call.member_answered | Your team member answered. |
click_to_call.customer_ringing | We are calling the customer. |
click_to_call.customer_answered | The customer answered: they are talking. |
click_to_call.held | The customer was put on hold through this API. |
click_to_call.resumed | The customer was taken off hold. |
click_to_call.transferred | The customer was handed on, by your system or the team member. |
click_to_call.ended | The click to call is over. Sent exactly once, with the outcome. |
click_to_call.tagged | The team member saved an outcome, a note or a callback for the call. |
click_to_call.member_ringing
{
"id": "evt_0a1b2c3d4e5f40718293a4b5c6d7e8f9",
"object": "event",
"api_version": "2026-06-01",
"type": "click_to_call.member_ringing",
"created": 1791783062,
"occurred_at": "2026-10-12T05:31:02.000Z",
"timestamp_ist": "2026-10-12T11:01:02.000+05:30",
"org_id": 42,
"reference_id": "LEAD-2291",
"livemode": true,
"data": {
"id": "c2c_9f1c4e7a2b6d48e0a3c5f1d29b7e6a40",
"object": "click_to_call",
"state": "member_ringing",
"active": true,
"on_hold": false,
"mode": "internet",
"member": {
"id": 1042,
"name": "Neha Sharma"
},
"from_number": "+9111XXXXXX45",
"to_number": "+9198XXXXXX21",
"customer_name": "Ravi Kumar",
"call_id": null,
"reference_id": "LEAD-2291",
"metadata": {
"deal": "D-77",
"crm_owner": "neha"
},
"created_at": "2026-10-12T05:31:02.000Z",
"created_at_ist": "2026-10-12T11:01:02.000+05:30",
"member_answered_at": null,
"member_answered_at_ist": null,
"customer_answered_at": null,
"customer_answered_at_ist": null,
"ended_at": null,
"ended_at_ist": null,
"duration_sec": 0,
"outcome": null,
"ended_by": null,
"end_reason": null,
"transferred_to": null,
"recording_available": false,
"recording_url": null,
"disposition": null,
"sequence": 1,
"updated_at": "2026-10-12T05:31:02.000Z",
"updated_at_ist": "2026-10-12T11:01:02.000+05:30",
"dial_started_at": null,
"dial_started_at_ist": null,
"customer_ringing_at": null,
"customer_ringing_at_ist": null,
"member_ring_sec": null,
"customer_ring_sec": null,
"call_duration_sec": null,
"recording": {
"status": "none",
"revision": 0,
"url": null,
"format": null,
"size_bytes": null,
"duration_sec": null,
"channels": null,
"channel_layout": null,
"retention_days": 60,
"expires_at": "2026-12-11T05:31:02.000Z",
"parts": [],
"parts_ordered": false
}
},
"event": "click_to_call.member_ringing",
"event_id": "evt_0a1b2c3d4e5f40718293a4b5c6d7e8f9"
}click_to_call.member_answered
{
"id": "evt_1b2c3d4e5f60418293a4b5c6d7e8f90a",
"object": "event",
"api_version": "2026-06-01",
"type": "click_to_call.member_answered",
"created": 1791783069,
"occurred_at": "2026-10-12T05:31:09.204Z",
"timestamp_ist": "2026-10-12T11:01:09.204+05:30",
"org_id": 42,
"reference_id": "LEAD-2291",
"livemode": true,
"data": {
"id": "c2c_9f1c4e7a2b6d48e0a3c5f1d29b7e6a40",
"object": "click_to_call",
"state": "member_answered",
"active": true,
"on_hold": false,
"mode": "internet",
"member": {
"id": 1042,
"name": "Neha Sharma"
},
"from_number": "+9111XXXXXX45",
"to_number": "+9198XXXXXX21",
"customer_name": "Ravi Kumar",
"call_id": null,
"reference_id": "LEAD-2291",
"metadata": {
"deal": "D-77",
"crm_owner": "neha"
},
"created_at": "2026-10-12T05:31:02.000Z",
"created_at_ist": "2026-10-12T11:01:02.000+05:30",
"member_answered_at": "2026-10-12T05:31:09.204Z",
"member_answered_at_ist": "2026-10-12T11:01:09.204+05:30",
"customer_answered_at": null,
"customer_answered_at_ist": null,
"ended_at": null,
"ended_at_ist": null,
"duration_sec": 0,
"outcome": null,
"ended_by": null,
"end_reason": null,
"transferred_to": null,
"recording_available": false,
"recording_url": null,
"disposition": null,
"sequence": 2,
"updated_at": "2026-10-12T05:31:09.204Z",
"updated_at_ist": "2026-10-12T11:01:09.204+05:30",
"dial_started_at": null,
"dial_started_at_ist": null,
"customer_ringing_at": null,
"customer_ringing_at_ist": null,
"member_ring_sec": 7,
"customer_ring_sec": null,
"call_duration_sec": null,
"recording": {
"status": "none",
"revision": 0,
"url": null,
"format": null,
"size_bytes": null,
"duration_sec": null,
"channels": null,
"channel_layout": null,
"retention_days": 60,
"expires_at": "2026-12-11T05:31:02.000Z",
"parts": [],
"parts_ordered": false
}
},
"event": "click_to_call.member_answered",
"event_id": "evt_1b2c3d4e5f60418293a4b5c6d7e8f90a"
}click_to_call.customer_ringing
{
"id": "evt_2c3d4e5f607142938a4b5c6d7e8f90a1",
"object": "event",
"api_version": "2026-06-01",
"type": "click_to_call.customer_ringing",
"created": 1791783071,
"occurred_at": "2026-10-12T05:31:11.000Z",
"timestamp_ist": "2026-10-12T11:01:11.000+05:30",
"org_id": 42,
"reference_id": "LEAD-2291",
"livemode": true,
"data": {
"id": "c2c_9f1c4e7a2b6d48e0a3c5f1d29b7e6a40",
"object": "click_to_call",
"state": "customer_ringing",
"active": true,
"on_hold": false,
"mode": "internet",
"member": {
"id": 1042,
"name": "Neha Sharma"
},
"from_number": "+9111XXXXXX45",
"to_number": "+9198XXXXXX21",
"customer_name": "Ravi Kumar",
"call_id": "6b1e2f3a-4c5d-4e6f-8a7b-9c0d1e2f3a4b",
"reference_id": "LEAD-2291",
"metadata": {
"deal": "D-77",
"crm_owner": "neha"
},
"created_at": "2026-10-12T05:31:02.000Z",
"created_at_ist": "2026-10-12T11:01:02.000+05:30",
"member_answered_at": "2026-10-12T05:31:09.204Z",
"member_answered_at_ist": "2026-10-12T11:01:09.204+05:30",
"customer_answered_at": null,
"customer_answered_at_ist": null,
"ended_at": null,
"ended_at_ist": null,
"duration_sec": 0,
"outcome": null,
"ended_by": null,
"end_reason": null,
"transferred_to": null,
"recording_available": false,
"recording_url": null,
"disposition": null,
"sequence": 3,
"updated_at": "2026-10-12T05:31:09.204Z",
"updated_at_ist": "2026-10-12T11:01:09.204+05:30",
"dial_started_at": "2026-10-12T05:31:10.000Z",
"dial_started_at_ist": "2026-10-12T11:01:10.000+05:30",
"customer_ringing_at": "2026-10-12T05:31:10.400Z",
"customer_ringing_at_ist": "2026-10-12T11:01:10.400+05:30",
"member_ring_sec": 7,
"customer_ring_sec": null,
"call_duration_sec": null,
"recording": {
"status": "processing",
"revision": 0,
"url": null,
"format": null,
"size_bytes": null,
"duration_sec": null,
"channels": null,
"channel_layout": null,
"retention_days": 60,
"expires_at": "2026-12-11T05:31:10.000Z",
"parts": [],
"parts_ordered": false
}
},
"event": "click_to_call.customer_ringing",
"event_id": "evt_2c3d4e5f607142938a4b5c6d7e8f90a1"
}click_to_call.customer_answered
{
"id": "evt_3d4e5f6071824a93b4c5d6e7f80a1b2c",
"object": "event",
"api_version": "2026-06-01",
"type": "click_to_call.customer_answered",
"created": 1791783079,
"occurred_at": "2026-10-12T05:31:19.516Z",
"timestamp_ist": "2026-10-12T11:01:19.516+05:30",
"org_id": 42,
"reference_id": "LEAD-2291",
"livemode": true,
"data": {
"id": "c2c_9f1c4e7a2b6d48e0a3c5f1d29b7e6a40",
"object": "click_to_call",
"state": "in_progress",
"active": true,
"on_hold": false,
"mode": "internet",
"member": {
"id": 1042,
"name": "Neha Sharma"
},
"from_number": "+9111XXXXXX45",
"to_number": "+9198XXXXXX21",
"customer_name": "Ravi Kumar",
"call_id": "6b1e2f3a-4c5d-4e6f-8a7b-9c0d1e2f3a4b",
"reference_id": "LEAD-2291",
"metadata": {
"deal": "D-77",
"crm_owner": "neha"
},
"created_at": "2026-10-12T05:31:02.000Z",
"created_at_ist": "2026-10-12T11:01:02.000+05:30",
"member_answered_at": "2026-10-12T05:31:09.204Z",
"member_answered_at_ist": "2026-10-12T11:01:09.204+05:30",
"customer_answered_at": "2026-10-12T05:31:19.516Z",
"customer_answered_at_ist": "2026-10-12T11:01:19.516+05:30",
"ended_at": null,
"ended_at_ist": null,
"duration_sec": 0,
"outcome": null,
"ended_by": null,
"end_reason": null,
"transferred_to": null,
"recording_available": false,
"recording_url": null,
"disposition": null,
"sequence": 4,
"updated_at": "2026-10-12T05:31:19.516Z",
"updated_at_ist": "2026-10-12T11:01:19.516+05:30",
"dial_started_at": "2026-10-12T05:31:10.000Z",
"dial_started_at_ist": "2026-10-12T11:01:10.000+05:30",
"customer_ringing_at": "2026-10-12T05:31:10.400Z",
"customer_ringing_at_ist": "2026-10-12T11:01:10.400+05:30",
"member_ring_sec": 7,
"customer_ring_sec": 9,
"call_duration_sec": null,
"recording": {
"status": "processing",
"revision": 0,
"url": null,
"format": null,
"size_bytes": null,
"duration_sec": null,
"channels": null,
"channel_layout": null,
"retention_days": 60,
"expires_at": "2026-12-11T05:31:10.000Z",
"parts": [],
"parts_ordered": false
}
},
"event": "click_to_call.customer_answered",
"event_id": "evt_3d4e5f6071824a93b4c5d6e7f80a1b2c"
}click_to_call.held
{
"id": "evt_4e5f60718293a4b5c6d7e8f90a1b2c3d",
"object": "event",
"api_version": "2026-06-01",
"type": "click_to_call.held",
"created": 1791783130,
"occurred_at": "2026-10-12T05:32:10.000Z",
"timestamp_ist": "2026-10-12T11:02:10.000+05:30",
"org_id": 42,
"reference_id": "LEAD-2291",
"livemode": true,
"data": {
"id": "c2c_9f1c4e7a2b6d48e0a3c5f1d29b7e6a40",
"object": "click_to_call",
"state": "in_progress",
"active": true,
"on_hold": true,
"mode": "internet",
"member": {
"id": 1042,
"name": "Neha Sharma"
},
"from_number": "+9111XXXXXX45",
"to_number": "+9198XXXXXX21",
"customer_name": "Ravi Kumar",
"call_id": "6b1e2f3a-4c5d-4e6f-8a7b-9c0d1e2f3a4b",
"reference_id": "LEAD-2291",
"metadata": {
"deal": "D-77",
"crm_owner": "neha"
},
"created_at": "2026-10-12T05:31:02.000Z",
"created_at_ist": "2026-10-12T11:01:02.000+05:30",
"member_answered_at": "2026-10-12T05:31:09.204Z",
"member_answered_at_ist": "2026-10-12T11:01:09.204+05:30",
"customer_answered_at": "2026-10-12T05:31:19.516Z",
"customer_answered_at_ist": "2026-10-12T11:01:19.516+05:30",
"ended_at": null,
"ended_at_ist": null,
"duration_sec": 51,
"outcome": null,
"ended_by": null,
"end_reason": null,
"transferred_to": null,
"recording_available": false,
"recording_url": null,
"disposition": null,
"sequence": 5,
"updated_at": "2026-10-12T05:31:19.516Z",
"updated_at_ist": "2026-10-12T11:01:19.516+05:30",
"dial_started_at": "2026-10-12T05:31:10.000Z",
"dial_started_at_ist": "2026-10-12T11:01:10.000+05:30",
"customer_ringing_at": "2026-10-12T05:31:10.400Z",
"customer_ringing_at_ist": "2026-10-12T11:01:10.400+05:30",
"member_ring_sec": 7,
"customer_ring_sec": 9,
"call_duration_sec": null,
"recording": {
"status": "processing",
"revision": 0,
"url": null,
"format": null,
"size_bytes": null,
"duration_sec": null,
"channels": null,
"channel_layout": null,
"retention_days": 60,
"expires_at": "2026-12-11T05:31:10.000Z",
"parts": [],
"parts_ordered": false
}
},
"event": "click_to_call.held",
"event_id": "evt_4e5f60718293a4b5c6d7e8f90a1b2c3d"
}click_to_call.resumed
{
"id": "evt_5f60718293a4b5c6d7e8f90a1b2c3d4e",
"object": "event",
"api_version": "2026-06-01",
"type": "click_to_call.resumed",
"created": 1791783160,
"occurred_at": "2026-10-12T05:32:40.000Z",
"timestamp_ist": "2026-10-12T11:02:40.000+05:30",
"org_id": 42,
"reference_id": "LEAD-2291",
"livemode": true,
"data": {
"id": "c2c_9f1c4e7a2b6d48e0a3c5f1d29b7e6a40",
"object": "click_to_call",
"state": "in_progress",
"active": true,
"on_hold": false,
"mode": "internet",
"member": {
"id": 1042,
"name": "Neha Sharma"
},
"from_number": "+9111XXXXXX45",
"to_number": "+9198XXXXXX21",
"customer_name": "Ravi Kumar",
"call_id": "6b1e2f3a-4c5d-4e6f-8a7b-9c0d1e2f3a4b",
"reference_id": "LEAD-2291",
"metadata": {
"deal": "D-77",
"crm_owner": "neha"
},
"created_at": "2026-10-12T05:31:02.000Z",
"created_at_ist": "2026-10-12T11:01:02.000+05:30",
"member_answered_at": "2026-10-12T05:31:09.204Z",
"member_answered_at_ist": "2026-10-12T11:01:09.204+05:30",
"customer_answered_at": "2026-10-12T05:31:19.516Z",
"customer_answered_at_ist": "2026-10-12T11:01:19.516+05:30",
"ended_at": null,
"ended_at_ist": null,
"duration_sec": 81,
"outcome": null,
"ended_by": null,
"end_reason": null,
"transferred_to": null,
"recording_available": false,
"recording_url": null,
"disposition": null,
"sequence": 6,
"updated_at": "2026-10-12T05:31:19.516Z",
"updated_at_ist": "2026-10-12T11:01:19.516+05:30",
"dial_started_at": "2026-10-12T05:31:10.000Z",
"dial_started_at_ist": "2026-10-12T11:01:10.000+05:30",
"customer_ringing_at": "2026-10-12T05:31:10.400Z",
"customer_ringing_at_ist": "2026-10-12T11:01:10.400+05:30",
"member_ring_sec": 7,
"customer_ring_sec": 9,
"call_duration_sec": null,
"recording": {
"status": "processing",
"revision": 0,
"url": null,
"format": null,
"size_bytes": null,
"duration_sec": null,
"channels": null,
"channel_layout": null,
"retention_days": 60,
"expires_at": "2026-12-11T05:31:10.000Z",
"parts": [],
"parts_ordered": false
}
},
"event": "click_to_call.resumed",
"event_id": "evt_5f60718293a4b5c6d7e8f90a1b2c3d4e"
}click_to_call.transferred
transferred_to says to whom: always when your system transferred it, and when the team member did, whenever we know the colleague or number. Followed at once by click_to_call.ended with the same object, one sequence higher. On a move to a call menu its recording ends at the move; the call menu part is a call of its own, lane menu.{
"id": "evt_60718293a4b5c6d7e8f90a1b2c3d4e5f",
"object": "event",
"api_version": "2026-06-01",
"type": "click_to_call.transferred",
"created": 1791785012,
"occurred_at": "2026-10-12T06:03:32.000Z",
"timestamp_ist": "2026-10-12T11:33:32.000+05:30",
"org_id": 42,
"reference_id": "LEAD-2304",
"livemode": true,
"data": {
"id": "c2c_4d7b1e9c3a5f42d8b6e0c2a1f9d37b58",
"object": "click_to_call",
"state": "transferred",
"active": false,
"on_hold": false,
"mode": "phone_bridge",
"member": {
"id": 1057,
"name": "Arjun Mehta"
},
"from_number": "+9179XXXXXX12",
"to_number": "+9197XXXXXX08",
"customer_name": "Meera Iyer",
"call_id": "8d3f4b5c-6e7a-4b8c-9d0e-1f2a3b4c5d6e",
"reference_id": "LEAD-2304",
"metadata": {
"deal": "D-81"
},
"created_at": "2026-10-12T06:02:14.000Z",
"created_at_ist": "2026-10-12T11:32:14.000+05:30",
"member_answered_at": "2026-10-12T06:02:20.418Z",
"member_answered_at_ist": "2026-10-12T11:32:20.418+05:30",
"customer_answered_at": "2026-10-12T06:02:31.612Z",
"customer_answered_at_ist": "2026-10-12T11:32:31.612+05:30",
"ended_at": "2026-10-12T06:03:32.000Z",
"ended_at_ist": "2026-10-12T11:33:32.000+05:30",
"duration_sec": 61,
"outcome": "transferred",
"ended_by": "api",
"end_reason": "transferred",
"transferred_to": {
"kind": "member",
"member_id": 1042,
"member_name": "Neha Sharma",
"menu_id": null,
"menu_name": null,
"number": null
},
"recording_available": false,
"recording_url": null,
"disposition": null,
"sequence": 5,
"updated_at": "2026-10-12T06:03:32.000Z",
"updated_at_ist": "2026-10-12T11:33:32.000+05:30",
"dial_started_at": null,
"dial_started_at_ist": null,
"customer_ringing_at": "2026-10-12T06:02:20.418Z",
"customer_ringing_at_ist": "2026-10-12T11:32:20.418+05:30",
"member_ring_sec": 6,
"customer_ring_sec": 11,
"call_duration_sec": 248,
"recording": {
"status": "processing",
"revision": 0,
"url": null,
"format": null,
"size_bytes": null,
"duration_sec": null,
"channels": null,
"channel_layout": null,
"retention_days": 60,
"expires_at": "2026-12-11T06:02:14.000Z",
"parts": [],
"parts_ordered": false
}
},
"event": "click_to_call.transferred",
"event_id": "evt_60718293a4b5c6d7e8f90a1b2c3d4e5f"
}click_to_call.ended
outcome, ended_by and duration_sec. This is the event to build on. When the call itself ends (either side hangs up, or your hang up request once a call is on the line), the outcome and this event come about five seconds after the end, like call.completed: we wait for the call's last facts, such as who hung up. Until then the click to call stays active. A transfer you request, and over the internet a cancel before the customer is called or a ring the team member does not answer or declines, reach their outcome at once.{
"id": "evt_718293a4b5c6d7e8f90a1b2c3d4e5f60",
"object": "event",
"api_version": "2026-06-01",
"type": "click_to_call.ended",
"created": 1791783173,
"occurred_at": "2026-10-12T05:32:53.000Z",
"timestamp_ist": "2026-10-12T11:02:53.000+05:30",
"org_id": 42,
"reference_id": "LEAD-2291",
"livemode": true,
"data": {
"id": "c2c_9f1c4e7a2b6d48e0a3c5f1d29b7e6a40",
"object": "click_to_call",
"state": "completed",
"active": false,
"on_hold": false,
"mode": "internet",
"member": {
"id": 1042,
"name": "Neha Sharma"
},
"from_number": "+9111XXXXXX45",
"to_number": "+9198XXXXXX21",
"customer_name": "Ravi Kumar",
"call_id": "6b1e2f3a-4c5d-4e6f-8a7b-9c0d1e2f3a4b",
"reference_id": "LEAD-2291",
"metadata": {
"deal": "D-77",
"crm_owner": "neha"
},
"created_at": "2026-10-12T05:31:02.000Z",
"created_at_ist": "2026-10-12T11:01:02.000+05:30",
"member_answered_at": "2026-10-12T05:31:09.204Z",
"member_answered_at_ist": "2026-10-12T11:01:09.204+05:30",
"customer_answered_at": "2026-10-12T05:31:19.516Z",
"customer_answered_at_ist": "2026-10-12T11:01:19.516+05:30",
"ended_at": "2026-10-12T05:32:53.000Z",
"ended_at_ist": "2026-10-12T11:02:53.000+05:30",
"duration_sec": 94,
"outcome": "completed",
"ended_by": "customer",
"end_reason": "customer_ended",
"transferred_to": null,
"recording_available": false,
"recording_url": null,
"disposition": null,
"sequence": 7,
"updated_at": "2026-10-12T05:32:53.000Z",
"updated_at_ist": "2026-10-12T11:02:53.000+05:30",
"dial_started_at": "2026-10-12T05:31:10.000Z",
"dial_started_at_ist": "2026-10-12T11:01:10.000+05:30",
"customer_ringing_at": "2026-10-12T05:31:10.400Z",
"customer_ringing_at_ist": "2026-10-12T11:01:10.400+05:30",
"member_ring_sec": 7,
"customer_ring_sec": 9,
"call_duration_sec": 94,
"recording": {
"status": "processing",
"revision": 0,
"url": null,
"format": null,
"size_bytes": null,
"duration_sec": null,
"channels": null,
"channel_layout": null,
"retention_days": 60,
"expires_at": "2026-12-11T05:31:10.000Z",
"parts": [],
"parts_ordered": false
}
},
"event": "click_to_call.ended",
"event_id": "evt_718293a4b5c6d7e8f90a1b2c3d4e5f60"
}click_to_call.tagged
disposition carries it. Usually after click_to_call.ended, and again if they change it.{
"id": "evt_8293a4b5c6d7e8f90a1b2c3d4e5f6071",
"object": "event",
"api_version": "2026-06-01",
"type": "click_to_call.tagged",
"created": 1791783245,
"occurred_at": "2026-10-12T05:34:05.000Z",
"timestamp_ist": "2026-10-12T11:04:05.000+05:30",
"org_id": 42,
"reference_id": "LEAD-2291",
"livemode": true,
"data": {
"id": "c2c_9f1c4e7a2b6d48e0a3c5f1d29b7e6a40",
"object": "click_to_call",
"state": "completed",
"active": false,
"on_hold": false,
"mode": "internet",
"member": {
"id": 1042,
"name": "Neha Sharma"
},
"from_number": "+9111XXXXXX45",
"to_number": "+9198XXXXXX21",
"customer_name": "Ravi Kumar",
"call_id": "6b1e2f3a-4c5d-4e6f-8a7b-9c0d1e2f3a4b",
"reference_id": "LEAD-2291",
"metadata": {
"deal": "D-77",
"crm_owner": "neha"
},
"created_at": "2026-10-12T05:31:02.000Z",
"created_at_ist": "2026-10-12T11:01:02.000+05:30",
"member_answered_at": "2026-10-12T05:31:09.204Z",
"member_answered_at_ist": "2026-10-12T11:01:09.204+05:30",
"customer_answered_at": "2026-10-12T05:31:19.516Z",
"customer_answered_at_ist": "2026-10-12T11:01:19.516+05:30",
"ended_at": "2026-10-12T05:32:53.000Z",
"ended_at_ist": "2026-10-12T11:02:53.000+05:30",
"duration_sec": 94,
"outcome": "completed",
"ended_by": "customer",
"end_reason": "customer_ended",
"transferred_to": null,
"recording_available": true,
"recording_url": "https://voice.agentive.co.in/api/public/v1/calls/6b1e2f3a-4c5d-4e6f-8a7b-9c0d1e2f3a4b/recording",
"disposition": {
"outcome": "callback",
"note": "Wants the Gold plan quote on email first.",
"callback_at": "2026-10-14T05:30:00.000Z",
"tagged_at": "2026-10-12T05:34:05.000Z"
},
"sequence": 8,
"updated_at": "2026-10-12T05:34:05.000Z",
"updated_at_ist": "2026-10-12T11:04:05.000+05:30",
"dial_started_at": "2026-10-12T05:31:10.000Z",
"dial_started_at_ist": "2026-10-12T11:01:10.000+05:30",
"customer_ringing_at": "2026-10-12T05:31:10.400Z",
"customer_ringing_at_ist": "2026-10-12T11:01:10.400+05:30",
"member_ring_sec": 7,
"customer_ring_sec": 9,
"call_duration_sec": 94,
"recording": {
"status": "available",
"revision": 1,
"url": "https://voice.agentive.co.in/api/public/v1/calls/6b1e2f3a-4c5d-4e6f-8a7b-9c0d1e2f3a4b/recording",
"format": "ogg",
"size_bytes": 381952,
"duration_sec": 94.6,
"channels": 2,
"channel_layout": {
"left": "customer",
"right": "member"
},
"retention_days": 60,
"expires_at": "2026-12-11T05:31:10.000Z",
"parts": [
{
"part": 1,
"primary": true,
"segment": null,
"url": "https://voice.agentive.co.in/api/public/v1/calls/6b1e2f3a-4c5d-4e6f-8a7b-9c0d1e2f3a4b/recording",
"format": "ogg",
"size_bytes": 381952,
"duration_sec": 94.6,
"channels": 2,
"started_at": "2026-10-12T05:31:19.516Z",
"member": null
}
],
"parts_ordered": true
}
},
"event": "click_to_call.tagged",
"event_id": "evt_8293a4b5c6d7e8f90a1b2c3d4e5f6071"
}Ordering and de-duplication
- Events can arrive out of order, and a retry can arrive after a newer event. Keep a click to call's state only from the event with the highest
data.sequenceyou have seen. - De-duplicate on the event
id: our retries repeat it. - The order of the events is
member_ringing,member_answered,customer_ringing,customer_answered, then any number ofheldandresumed, thentransferredif it happened, thenended. A call that never got far is justmember_ringingthenended.taggedcan come at any time once we have started calling the customer, usually afterended. - When a call moves through several states at once (a very short call, or one we caught up with late), you still receive every event, in that order. Each event has its own
sequence. It goes up with every change to the call, and some changes send no event (the recording becoming ready, for one), so the numbers can skip: a gap is not a missed event. Each event's object shows the call as it was at that step, so a step's later facts (answer times,outcome,ended_at) are not in it yet. The event with the highestsequenceholds the latest picture. click_to_call.endedis sent exactly once. Missed one? Read the click to call, or list withupdated_since.
Call and recording events
From the moment we call the customer, a click to call has a call record (its call_id), and that record appears in your account's other events like any call on your account: the call.* events, call.tagged, recording.available and the others you tick. On the click to call's own record, data.unique_id is the click to call id, and the call object carries click_to_call_id, your reference_id and your metadata. Follow the click to call with the click_to_call.* events; use recording.available to learn when the recording is complete. After a transfer, the call record goes on until the customer's whole call is over, and its single terminal event comes then, with the whole call's times.
A transfer into a call menu is the one exception. If your team member moves the customer into a call menu, the rest of the call is a record of its own (lane menu, its own call_id and unique_id) with its own call.* events and recording; the click to call's record names it in transfers[].to_call_id, and click_to_call.ended stays the click to call's outcome. That record sends no call.ringing, and its call.answered comes only when a team member picks up from the menu or the number a menu key forwards the call to answers. One that a WhatsApp key, or a key that hands the call to an AI voice agent, connected ends call.completed with the move as its answered_at and no call.answered before it; one that nobody answered ends call.no_answer. The click to call's own record then usually sends its terminal call.* event a few seconds after the move, at the latest when the customer's call ends (since 14 October 2026; before, always when the customer's call ended).
Since 17 October 2026 the call menu record of a click to call (named in its transfers[].to_call_id) also carries the click to call's click_to_call_id, reference_id and metadata, and via_api true, on its events, its recording.available and every read (before, it carried none of them; a call menu record from before then can show them on a read too, at its unchanged version, and its events are not sent again). One click to call can then have two records with the same click_to_call_id, so an event with a set click_to_call_id can be the call menu record's: the click to call's own record is the one whose unique_id is the click to call id. Match the click to call's own record by unique_id, and take the click to call's outcome from click_to_call.ended, never from the call menu record's terminal event. The call menu record's recording is the call menu part of the call: keep it beside the click to call's own recording, never in place of it.
Over the internet, a click to call that ended before the customer was called has no call record and sends no call.* event. On Phone Bridge the call record starts when the team member's mobile is rung: if they decline or do not answer, that record still sends a final call.* event whose to_number is the customer, even though the customer was never called. Use the click_to_call.* outcome (member_declined or member_no_answer) to tell who did not answer. A Phone Bridge call your team member answered and the customer did not is call.no_answer, never call.completed.
Your whole call history
The Telephony API reads every call on your account, not only the click to calls: the calls your team makes from the panel, the app, their own SIM or Phone Bridge, and every inbound call, menu calls included. Each one is a complete call record: the team member, the answer and end times, talk, wait and hold time, the outcome detail, who ended the call, the recording with every part, the outcome and note your team saved, voicemail, the call menu key, who it rang and every transfer.
- Keep it in sync with the change feed (
GET /calls?updated_since=...) and the call events. - Read one call with
GET /calls/{id}using the click to call id, acall_idor any id an event gave you. - Filter the calls of one team member with
member_id(a call they handed on to a colleague who answered it is listed under that colleague), of one of your records withreference_id, or of one click to call withclick_to_call_id. Since 17 October 2026 both also return a click to call's call menu record (forreference_id, of the first 50 click to calls placed with it); tell the two records apart byunique_id. - Download recordings and voicemails with your keys, before their
expires_at.
Verifying a webhook
Verify every delivery before you trust it. Read the X-Agentive-Signature-V2 header, which looks like t=1791783173,v2=5f0c...:
- Read the raw bodyTake the body as bytes, before any JSON parsing.
- Check the timeReject the delivery if
tis more than 300 seconds from your own clock. - Compute the signatureHMAC-SHA256 with the endpoint's signing secret over
t, a dot, then the raw body, hex-encoded. - Compare safelyCompare it with
v2in constant time, after checking that the two are the same length in bytes. Answer 401 on any mismatch, and on a delivery with noX-Agentive-Signature-V2at all. - De-duplicate, then keep the newestRecord the event
idand ignore one you have already handled, then keep the click to call only when itssequenceis newer than the one you hold.
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 / saveIfNewer: your own store, keyed as noted.
if (await alreadyHandled(evt.id)) return res.sendStatus(200); // our retries repeat the id
await remember(evt.id);
if (evt.type.startsWith("click_to_call.")) {
const c = evt.data;
// Events can arrive out of order: keep a call's state only when its sequence is newer.
await saveIfNewer(c.id, c.sequence, c);
if (evt.type === "click_to_call.ended") console.log(c.reference_id, c.outcome, c.duration_sec);
}
res.sendStatus(200);
});
app.listen(3000);X-Agentive-Signature that signs the body alone. Do not accept it on its own: it cannot tell a fresh delivery from one captured and sent again later. Never run a verifier with an empty secret; the samples refuse to start without one.To check your code by hand, compute a signature from the shell:
# AGENTIVE_WEBHOOK_SECRET must already be set in your environment.
T=1791783173
BODY='{"id":"evt_abc","type":"click_to_call.ended","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. Every refusal on these routes, except a refusal of the keys themselves, also carries retryable: true when the same request can work later, false when it will not until something changes. A Retry-After header (and retry_after_sec) comes only with a retryable refusal, and not with every one: insufficient_balance, call_not_placed and service_unavailable carry none, so choose your own pause. A refusal for a bad field adds field.
| error_code | HTTP | retryable | Means | What to do |
|---|---|---|---|---|
| validation_error | 400 | No | A field is missing, has the wrong type or breaks a rule, or the body carries a field the API does not take. field names it, for example agent_id in "Unknown field: agent_id". | Fix the request, then send it with a new Idempotency-Key (a 400 is kept for the key). |
| invalid_number | 400 | No | to (or to_number on a transfer) is not a number we can dial in India, or it is one of your own account's numbers. | Fix the number. |
| international_not_supported | 400 | No | The number is outside India, or your account's filter for numbers outside India refuses it. | Call only Indian numbers. |
| invalid_caller_id | 400 | No | caller_id is not one of the team member's numbers. | Send one of the member's numbers, or leave it out to use their default. |
| member_not_found | 404 | No | No team member with that id or email on your account. Also answered for a colleague named on a transfer. | Check the id or email with List team members. |
| member_cannot_call | 422 | No | The team member cannot place calls at all. details names why: no calling number, an app-only member, a paused member, a Phone Bridge mobile number that is not verified, or a role that does not place calls (unavailable_reason on the member says the same). | Fix it in Team Members, or pick another member. |
| member_unavailable | 409 | Yes | The team member is Offline or on a break, or (calls over the internet) has no web panel or app signed in. details says which. Nothing rang. | Ask them to go Available or sign in, or call another member. Retry after Retry-After. |
| member_on_call | 409 | Yes | The team member is on a call, or is already being rung by another click to call or a campaign. | Retry after Retry-After, when `ringable_now` on the member is true again. |
| member_cooldown | 429 | Yes | The team member missed or declined two click to calls in a row in the last 10 minutes. | Wait for Retry-After (when the cooldown ends), or call another member. |
| member_limit | 429 | Yes | The team member was rung 60 times in the last hour, or 300 times today (IST). | Wait for Retry-After, or call another member. |
| number_flood | 429 | Yes | 3 calls through the API to this number in 10 minutes, or 10 today (IST), counted across every API that places calls. Both limits answer this one code; details says which. | Wait for Retry-After (up to 600 seconds, or until midnight IST). Do not retry sooner. |
| rate_limited | 429 | Yes | Your request budget, the reading budget of a live click to call, or your account's calls a minute, is spent. | Wait for Retry-After. |
| concurrency_limit | 429 | Yes | Your account's line limit is reached by the calls live right now. A Phone Bridge click to call counts as one line when it is placed; a click to call over the internet is not counted, like any call a team member places from the panel or app. | Retry after Retry-After. |
| insufficient_balance | 402 | Yes | Your account's outgoing minutes for today are used up and its balance cannot cover more. No Retry-After is sent: it clears when you top up. | Top up, then retry with the same Idempotency-Key. |
| number_blocked | 422 | No | The number is on the do not disturb list, or on your account's or the team member's block list. | Do not call this number. |
| outbound_blocked | 422 | No | Outgoing calls are switched off on your account, the account is paused, or its plan has ended. | Contact your account manager. |
| verification_required | 422 | No | Your account cannot place calls until its business verification is complete. | Finish business verification in the dashboard. |
| service_unavailable | 503 | Yes | The line for this caller ID cannot be reached right now, the call could not be reached to carry out a control, or the record could not be read. No Retry-After is sent. When the body says retryable: false (details "Click to call is switched off for this account."), click to call is not switched on for your account right now: every route answers this, before the Telephony API is checked. | Wait about 30 seconds (add some jitter), then try again. After a keypad or transfer request we cannot be sure nothing was done: read the click to call first and use a new Idempotency-Key. If retryable is false, do not retry: contact your account manager. |
| call_not_placed | 502 | Yes | We could not place the call, or carry out the control, on our side. On a start, a keypad or a transfer request the Idempotency-Key is then used up: sending it again replays call_state_unknown. A start refused this way carries no unique_id. | Read the click to call (on a start, list by member_id or reference_id with created_from) before you try again, then use a new Idempotency-Key. |
| call_not_connected | 409 | Yes | Keypad, hold or transfer was sent before the customer answered. No leg was touched. | Wait for click_to_call.customer_answered, then send it again. |
| call_on_hold | 409 | Yes | Keypad tones were sent while the call is on hold through this API (a hold the team member made from their panel or app is not known here). | Resume the call first. |
| call_ended | 410 | No | The click to call has ended. | Nothing to do. Hang up on an ended call answers 200 instead. |
| call_transferred | 410 | No | The click to call was transferred: the customer is now with a colleague, a call menu or another number. | Follow the rest of the call through the account's call events. |
| target_on_call | 409 | Yes | The colleague you are transferring to is on another call. | Try again in a moment, or pick someone else. |
| target_unavailable | 409 | Yes | The colleague is Offline, on a break, or cannot be rung right now. | Pick someone else, or a call menu. |
| menu_not_found | 404 | No | No call menu with that id on your account. | Check the id with List call menus. |
| feature_disabled | 403 | No | Your account does not have the Telephony API, or (on a transfer) the kind of transfer you asked for: transfer to a call menu, or to an outside number. | Contact your account manager. |
| not_found | 404 | No | No click to call with that id on your account. | Check the id. |
| idempotency_in_progress | 409 | Yes | A request with the same Idempotency-Key is still running. | Wait for Retry-After and send it again with the same key. |
| idempotency_conflict | 422 | No | The same Idempotency-Key was sent with a different body. | Use a new key for a new request. |
| call_state_unknown | 502 | No | The replay of a request that failed on our side after the work had started: we cannot say whether it took effect. Sending the same key again only replays it, for 24 hours. | Read the click to call (or list by reference_id) before you try again with a new key. |
| missing_credentials | 401 | Not set | No publishable key was sent. | Send both keys. |
| invalid_credentials | 401 | Not set | Unknown key, or a wrong secret. | Check the keys; rotate if unsure. |
| account_inactive | 403 | Not set | The account is not active. | Contact your account manager. |
| api_disabled | 403 | Not set | API access is not enabled for the account. | Contact your account manager. |
Not set means the keys were refused, so the body carries no retryable: fix the keys or the account before you try again. Treat an error_code you do not recognise as a generic failure of its HTTP class: 409 and 429 are worth retrying after Retry-After, a 503 after a pause of your own of about 30 seconds, the rest are not.
Limits and polling
Limits protect your customers, your team and the phone network. Each one refuses with a clear error_code, and all but insufficient_balance also carry a Retry-After, because they can work later (top up first, then retry, for insufficient_balance).
| Limit | Scope | error_code |
|---|---|---|
| One click to call at a time. A team member on any call, or being rung by a campaign, is refused too. | Team member | member_on_call |
| 60 rings an hour, and 300 a day (IST) | Team member | member_limit |
| A 10 minute pause after 2 missed or declined click to calls in a row | Team member | member_cooldown |
| 3 calls through the API to one number in 10 minutes, and 10 in a day (IST), across every API that places calls | Account and number | number_flood |
| Your account's line limit: calls live at the same time. A Phone Bridge click to call counts as one when it is placed; a click to call over the internet is not counted (each team member can have only one at a time). | Account | concurrency_limit |
| Your account's calls a minute, and 120 requests a minute each for reads and for writes | Account | rate_limited |
| Your plan's outgoing minutes for the day, then your balance | Account | insufficient_balance |
Polling
Read a live click to call every 3 seconds at most, and stop once active is false. A call that ends on the line stays active for about five seconds before it reaches its outcome (see Hang up). Reading a live click to call has its own budget of 2 requests a second for each call, counted apart from your account's 120 reads a minute, so a CRM with many calls in progress does not run out. Reading an ended one counts as a normal read. Webhooks are the better way to wait.
To show who is free to take a call, read GET /members (its presence, on_call and ringable_now). Once every 15 to 30 seconds is enough, and stays well inside your 120 reads a minute.
Dry run
Send "dry_run": true with a start request to run every check without ringing anyone. If the call would go ahead you get 200 with the team member, the lane they would be rung on and the caller ID the customer would see. If it would be refused you get the same refusal a real request would. No click to call is created and no event is sent. Use it to test your integration, or to check a team member and a number before showing a Call button.
{
"member_email": "neha@example.in",
"to": "98XXXXXX21",
"reference_id": "LEAD-2291",
"dry_run": true
}{
"status": "success",
"code": 200,
"unique_id": null,
"reference_id": "LEAD-2291",
"details": "Every check passed. Nothing was rung.",
"dry_run": {
"member": {
"id": 1042,
"name": "Neha Sharma"
},
"mode": "internet",
"caller_id": "+9111XXXXXX45"
}
}Security
- Keys on your server only. A key can start calls from your team to any number. Keep both keys in environment variables or a secrets store on your server, and put a server between every page or app and this API.
- Verify every webhook with the version 2 signature, within five minutes, and refuse anything else. See Verifying a webhook.
- Decide the number on your server. Take the number to call from your own records, not from whatever the page or app sends.
Rotating your secret
Rotate the secret from Developers > API Access. You choose what happens to the old one:
| Choice | When to use it |
|---|---|
| Stop the old secret now | The default, and the only safe choice if the secret may have leaked. You get a new publishable key and a new secret, and the old ones stop working at once: update both in your integration. |
| Keep it working for 24 hours | A planned change. Your publishable key stays the same and only the secret changes. The old and the new secret both work for 24 hours, so you can roll the new one out with no failed requests. End it early with Stop it now. |
Only one old secret is ever kept: a second rotation ends the first one's 24 hours. API Access shows when your keys were last used, so you can see that the old secret has gone quiet before you end it.
Idempotency
Send an Idempotency-Key header on every POST. If the network drops and you are not sure whether the request went through, send it again with the same key: you get the first answer back, with Idempotent-Replayed: true, and nobody is rung twice, even if our server restarted in between.
- The key is 1 to 128 visible ASCII characters, kept for 24 hours per account.
- The same key with a different body is refused with HTTP 422 and
idempotency_conflict. - A 2xx, 400, 404 or 422 answer is kept for the key and replayed however often you resend. After fixing the request, send it with a new key.
- A 401 is never kept for the key: the keys are checked before the key is looked at, so fix the credentials and resend with the same key.
- A 402, 403, 409, 410 or 429 does not use the key up: fix the cause, then resend with the same key.
- A 5xx raised after we started placing the call is kept for the key and replayed as
call_state_unknown, withretryable: false; so is any 5xx answering a keypad or transfer request, which may have been partly done. Read the click to call (list bymember_idorreference_idwhen you have no id) before trying again with a new key. Any other 5xx does not use the key up. - A second click to call with a different key, for a team member who is already being rung, is refused with
member_on_call; it never rings them twice.
The full rules are in the Voice Broadcast API; they are the same on every Voice endpoint.
What we send and keep
- A team member's full mobile number is never returned by these routes or in a team call's record, only its last four digits (an AI voice agent call keeps the numbers the AI API returns). Platform staff are never listed.
- Keypad digits you send are played, and never stored in call records, events or API logs.
- Your
reference_idandmetadataare echoed on every event; keep secrets and personal data out of them. - The recording is reached only through the API with your keys, never through a public link.
- A click to call's record is kept for 400 days, then deleted. Its recording follows your plan's recording retention:
recording.expires_atsays when, so copy it before then.
Compliance
- One person, one call. Click to call connects a team member, who answers first, with one customer. It is not for bulk calling or recorded messages: there is no list upload and no bulk endpoint, and the limits above stop anything that looks like one. For recorded messages use the Voice Broadcast API.
- Consent. Call people who expect to hear from your business, and stop at once when someone asks you to.
- Do not disturb. Numbers on the platform's do not disturb list, and on your account's or the team member's block list, are never called, whatever you send.
- Calling hours. Click to call has no set calling window, because your team member answers first, as when they press Call in the panel. Call customers at times they expect.
- Complaints. Complaints from people you call can lead to outgoing calls being switched off on your account.
Go-live checklist
Before your first real customer
- Both keys live in environment variables on your server, and no page or app ever holds them.
- Your users are mapped to team members by email, and the number to call comes from your own records.
- Every
POSTsends anIdempotency-Key, and acall_state_unknownanswer is checked before any new attempt. - Your code branches on
error_code, offers a retry only whenretryableis true, and waits forRetry-After(or about 30 seconds when there is none). - A webhook endpoint on
https://verifiesX-Agentive-Signature-V2with a five minute window, de-duplicates on the eventid, keeps the highestsequence, and answers 2xx quickly. - You store the outcome from
click_to_call.ended, and catch up withupdated_sinceafter any outage. - You tried
dry_run, then a real call from a test team member to your own number, and saw every event arrive.
Next steps
Continue with the references that share this base URL and account.