Agentive
Agentive
Cloud TelephonyAI Voice AgentAI Chat AgentAboutContact
80056 80053
Book a Demo
Agentive
Agentive

One platform for every Indian business. Cloud telephony, an AI voice agent, an AI chat agent and the WhatsApp Business API, all in one place. Answer every call and message, and go live in a day.

Follow Us

Product

  • Cloud Telephony
  • AI Voice Agent
  • AI Chat Agent

Company

  • About Us
  • Contact Us
  • Disclaimer

Legal

  • Privacy Policy
  • Data Deletion
  • Terms of Service
  • Refund Policy
  • Shipping Policy
Meta Business PartnerAvailable on Google PlayAvailable on the App Store

© 2026 Agentive. Owned and operated by Mannan Technologies Private Limited.

HomeDevelopersWebhooks
API ReferenceWebhooks

On this page

Introduction
How webhooks work
Setting up a webhook
Events
The event envelope
Per-event payloads
call.initiatedcall.ringingcall.answeredcall.completedcall.no_answercall.busycall.failedrecording.availableotp.sentotp.verifiedotp.failedotp.max_attempts
The call object
Status and end reasons
The recording object
The verification object
Headers
Verifying the signature
Responding
Retries and delivery
Ordering and idempotency
Testing
Versioning
Security checklist

Webhooks

Webhooks

Receive call and verification events in near real time instead of polling. Each delivery is a typed event object with a stable envelope and an HMAC signature you can verify.

Payload version
2026-06-01
Signature
HMAC-SHA256
Currency / time
INR / IST

Introduction

Webhooks let your systems receive call and verification events the moment they happen, instead of polling for status. When a call or verification code moves through its lifecycle, we send a signed HTTPS POST to the endpoint URLs you register.

Every event is delivered as a typed event object with a stable envelope and a payload specific to that event. You can verify each delivery with an HMAC signature, and use the event id to make your handler idempotent. All timestamps are in Indian Standard Time (IST), ISO 8601 format. Call charges are in Indian Rupees (INR).

Current payload version
The current payload version is 2026-06-01, sent as api_version on every event. See Versioning for our compatibility promise.

How webhooks work

  1. 1
    You register one or more webhook endpoints in your dashboard, each with a URL and a signing secret.
  2. 2
    You choose which events each endpoint should receive (or all events).
  3. 3
    When a matching event occurs, we POST a JSON event object to your URL with a signature header.
  4. 4
    Your server verifies the signature, processes the event, and replies with a 2xx status.
  5. 5
    Every delivery attempt is logged and visible in your dashboard.

Webhooks are sent for calls and verification requests created through the API, and for relevant calls on your account, when the event matches an endpoint's subscription.

Setting up a webhook

Use the Webhooks tab in your dashboard to:

  • Add an endpoint: enter an https:// URL and either supply your own signing secret or let one be generated.
  • Choose events: tick the events you want, or leave them all unticked to receive every event.
  • Inspect a sample payload for any event, so you can build your handler before the first live call.
  • Toggle active or inactive without deleting.
  • Send a test event to confirm your receiver and signature check work.
  • View the recent delivery log (event, response code, attempts, time in IST).

The signing secret is shown in full once, when the endpoint is created or when you rotate it. Afterwards it is masked. Keep it secret; it is what proves a request came from us.

Public HTTPS endpoints only
The endpoint URL must be a publicly reachable https:// address. Private, loopback, and internal addresses are rejected when you save the endpoint.

Events

There are twelve events, grouped by the resource they describe.

Call lifecycle

Each carries a call object.

EventObjectFires when
call.initiatedcallA call you placed through the API is accepted and queued.
call.ringingcallThe recipient's phone starts ringing. Inbound calls only (see the note in call.ringing).
call.answeredcallThe recipient answers and the call connects.
call.completedcallA call that was answered finishes. Carries the talk duration.
call.no_answercallThe call rang but the recipient never picked up. Safe to retry later.
call.busycallThe recipient was on another call or declined. The number is reachable, so a retry can succeed.
call.failedcallThe call could not be placed at all (an invalid number or a carrier problem). Retrying the same number is unlikely to help.

Recording

Carries a recording object.

EventObjectFires when
recording.availablerecordingThe call recording has finished processing and is ready to download. Sent after the call ends.

Verification

Each carries a verification object.

EventObjectFires when
otp.sentverificationA voice verification call is queued for delivery.
otp.verifiedverificationA verification code is confirmed correct.
otp.failedverificationA recipient entered an incorrect code. The verification is still open until it expires or runs out of attempts.
otp.max_attemptsverificationToo many incorrect attempts. The code is now locked and can no longer be verified. Send a new code to retry.

An endpoint subscribed to no specific events receives all of them. The Object column is the value of data.object for that event. Branch on it when you want one handler to cover several events.

A call that does not connect
A non-connect now fires the specific event for the reason: call.no_answer when it rang unanswered, call.busy when it was busy or declined, and call.failed only for a true non-connect such as an invalid number or a carrier problem. The exact reason is also in the call object's end_reason.

The event envelope

Every webhook body is a single JSON object with the same outer envelope. Only the data block changes between events.

JSON
{
  "id": "evt_4b8f2c1d6a3e4f0b9d7c2a1e5f6b8c0d",
  "object": "event",
  "api_version": "2026-06-01",
  "type": "call.completed",
  "created": 1780589528,
  "occurred_at": "2026-06-04T16:12:08.512Z",
  "timestamp_ist": "2026-06-04T21:42:08.512+05:30",
  "org_id": 42,
  "reference_id": "order-99213",
  "livemode": true,
  "data": {
    "object": "call",
    "unique_id": "run_4821",
    "call_id": "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
    "direction": "outbound",
    "from_number": "07965318581",
    "to_number": "+919812345678",
    "status": "completed",
    "duration_sec": 42,
    "end_reason": "completed",
    "campaign_id": 4821,
    "campaign_type": "audio_blast",
    "reference_id": "order-99213"
  },
  "event": "call.completed",
  "event_id": "evt_4b8f2c1d6a3e4f0b9d7c2a1e5f6b8c0d"
}
FieldTypeDescription
idstringUnique id for this event, prefixed evt_. Use it to make your handler idempotent.
objectstringAlways "event".
api_versionstringThe payload version used to build this event.
typestringThe event name in resource.action form.
creatednumberWhen the event was generated, as a Unix epoch timestamp in seconds.
occurred_atstringThe same instant in UTC, ISO 8601 (Z).
timestamp_iststringThe same instant in IST, ISO 8601 with a +05:30 offset.
org_idnumberYour account id.
reference_idstring | nullThe reference_id you supplied when you placed the call or sent the code. null if none.
livemodebooleantrue for real events.
dataobjectThe event-specific record. Its object field names the resource type (call or verification).
eventstringAlias of type. Kept for compatibility.
event_idstringAlias of id. Kept for compatibility.
Notes
  • id and event_id are the same value. Prefer id.
  • type and event are the same value. Prefer type.
  • reference_id appears both at the top level and inside data, so you can read it wherever is convenient.
  • New fields may be added over time. Ignore fields you do not recognise.

Per-event payloads

Each event carries a data object specific to that event. Call events carry a call object, the recording event carries a recording object, and verification events carry a verification object. Fields that are not yet known at that point in the lifecycle are null. Below is the exact data for each event, plus a full sample of the body we POST.

call.initiated

A call you placed through the API was accepted and queued for dialling. This is the first event for an API-triggered call. call_id is not assigned yet.

FieldValue at this event
status"queued"
call_idnull (assigned at call.ringing)
duration_secnull
end_reasonnull
call.initiated
{
  "id": "evt_a17c2f9b4d6e4a1c8f0b3d5e7a9c1b2d",
  "object": "event",
  "api_version": "2026-06-01",
  "type": "call.initiated",
  "created": 1780589500,
  "occurred_at": "2026-06-04T16:11:40.000Z",
  "timestamp_ist": "2026-06-04T21:41:40.000+05:30",
  "org_id": 42,
  "reference_id": "order-99213",
  "livemode": true,
  "data": {
    "object": "call",
    "unique_id": "run_4821",
    "call_id": null,
    "direction": "outbound",
    "from_number": "07965318581",
    "to_number": "+919812345678",
    "status": "queued",
    "duration_sec": null,
    "end_reason": null,
    "campaign_id": 4821,
    "campaign_type": "audio_blast",
    "reference_id": "order-99213"
  },
  "event": "call.initiated",
  "event_id": "evt_a17c2f9b4d6e4a1c8f0b3d5e7a9c1b2d"
}

call.ringing

The recipient's phone has started ringing. From this event onward call_id is present.

Sent for inbound calls (someone calling one of your numbers). Calls you place through campaigns, the API, or OTP do not include call.ringing, because the outbound leg has no reliable ringing signal. Those run call.initiated then call.answered then call.completed (or call.failed).
FieldValue at this event
status"ringing"
call_idpresent
duration_secnull
call.ringing
{
  "id": "evt_b28d3f0c5e7f5b2d9a1c4e6f8b0d2c3e",
  "object": "event",
  "api_version": "2026-06-01",
  "type": "call.ringing",
  "created": 1780589505,
  "occurred_at": "2026-06-04T16:11:45.000Z",
  "timestamp_ist": "2026-06-04T21:41:45.000+05:30",
  "org_id": 42,
  "reference_id": "order-99213",
  "livemode": true,
  "data": {
    "object": "call",
    "unique_id": "run_4821",
    "call_id": "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
    "direction": "outbound",
    "from_number": "07965318581",
    "to_number": "+919812345678",
    "status": "ringing",
    "duration_sec": null,
    "end_reason": null,
    "campaign_id": 4821,
    "campaign_type": "audio_blast",
    "reference_id": "order-99213"
  },
  "event": "call.ringing",
  "event_id": "evt_b28d3f0c5e7f5b2d9a1c4e6f8b0d2c3e"
}

call.answered

The recipient picked up and the call is now connected.

FieldValue at this event
status"in_progress"
duration_secnull (set on completion)
call.answered
{
  "id": "evt_c39e4a1d6f8a6c3e0b2d5f7a9c1e3d4f",
  "object": "event",
  "api_version": "2026-06-01",
  "type": "call.answered",
  "created": 1780589512,
  "occurred_at": "2026-06-04T16:11:52.000Z",
  "timestamp_ist": "2026-06-04T21:41:52.000+05:30",
  "org_id": 42,
  "reference_id": "order-99213",
  "livemode": true,
  "data": {
    "object": "call",
    "unique_id": "run_4821",
    "call_id": "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
    "direction": "outbound",
    "from_number": "07965318581",
    "to_number": "+919812345678",
    "status": "in_progress",
    "duration_sec": null,
    "end_reason": null,
    "campaign_id": 4821,
    "campaign_type": "audio_blast",
    "reference_id": "order-99213"
  },
  "event": "call.answered",
  "event_id": "evt_c39e4a1d6f8a6c3e0b2d5f7a9c1e3d4f"
}

call.completed

A call that was answered has finished. This is the event to use for billing, analytics, and "call done" logic. duration_sec is the talk time in seconds.

FieldValue at this event
status"completed"
duration_sectalk seconds (an integer, may be 0)
end_reasona short reason, for example "completed"
call.completed
{
  "id": "evt_4b8f2c1d6a3e4f0b9d7c2a1e5f6b8c0d",
  "object": "event",
  "api_version": "2026-06-01",
  "type": "call.completed",
  "created": 1780589528,
  "occurred_at": "2026-06-04T16:12:08.512Z",
  "timestamp_ist": "2026-06-04T21:42:08.512+05:30",
  "org_id": 42,
  "reference_id": "order-99213",
  "livemode": true,
  "data": {
    "object": "call",
    "unique_id": "run_4821",
    "call_id": "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
    "direction": "outbound",
    "from_number": "07965318581",
    "to_number": "+919812345678",
    "status": "completed",
    "duration_sec": 42,
    "end_reason": "completed",
    "campaign_id": 4821,
    "campaign_type": "audio_blast",
    "reference_id": "order-99213"
  },
  "event": "call.completed",
  "event_id": "evt_4b8f2c1d6a3e4f0b9d7c2a1e5f6b8c0d"
}

call.no_answer

The call rang but the recipient never picked up. Safe to retry later. This is a terminal event for the call.

FieldValue at this event
status"missed"
duration_sec0
end_reason"no_answer"
call.no_answer
{
  "id": "evt_0a1b2c3d4e5f60718293a4b5c6d7e8f9",
  "object": "event",
  "api_version": "2026-06-01",
  "type": "call.no_answer",
  "created": 1780589545,
  "occurred_at": "2026-06-04T16:12:25.000Z",
  "timestamp_ist": "2026-06-04T21:42:25.000+05:30",
  "org_id": 42,
  "reference_id": "order-99213",
  "livemode": true,
  "data": {
    "object": "call",
    "unique_id": "run_4821",
    "call_id": "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
    "direction": "outbound",
    "from_number": "07965318581",
    "to_number": "+919812345678",
    "status": "missed",
    "duration_sec": 0,
    "end_reason": "no_answer",
    "campaign_id": 4821,
    "campaign_type": "audio_blast",
    "reference_id": "order-99213"
  },
  "event": "call.no_answer",
  "event_id": "evt_0a1b2c3d4e5f60718293a4b5c6d7e8f9"
}

call.busy

The recipient was on another call or declined. The number is reachable, so a retry can succeed. This is a terminal event for the call. end_reason is "busy" when the line was busy and "rejected" when the recipient actively declined.

FieldValue at this event
status"missed"
duration_sec0
end_reason"busy" (or "rejected")
call.busy
{
  "id": "evt_1b2c3d4e5f60718293a4b5c6d7e8f9a0",
  "object": "event",
  "api_version": "2026-06-01",
  "type": "call.busy",
  "created": 1780589548,
  "occurred_at": "2026-06-04T16:12:28.000Z",
  "timestamp_ist": "2026-06-04T21:42:28.000+05:30",
  "org_id": 42,
  "reference_id": "order-99213",
  "livemode": true,
  "data": {
    "object": "call",
    "unique_id": "run_4821",
    "call_id": "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
    "direction": "outbound",
    "from_number": "07965318581",
    "to_number": "+919812345678",
    "status": "missed",
    "duration_sec": 0,
    "end_reason": "busy",
    "campaign_id": 4821,
    "campaign_type": "audio_blast",
    "reference_id": "order-99213"
  },
  "event": "call.busy",
  "event_id": "evt_1b2c3d4e5f60718293a4b5c6d7e8f9a0"
}

call.failed

The call could not be placed at all (an invalid number or a carrier problem). Retrying the same number is unlikely to help. A call that simply rang unanswered or was busy fires call.no_answer or call.busy instead, not call.failed.

FieldValue at this event
status"failed"
duration_sec0
end_reason"failed"
call.failed
{
  "id": "evt_d40f5b2e7a9b7d4f1c3e6a8b0d2f4e5a",
  "object": "event",
  "api_version": "2026-06-01",
  "type": "call.failed",
  "created": 1780589560,
  "occurred_at": "2026-06-04T16:12:40.000Z",
  "timestamp_ist": "2026-06-04T21:42:40.000+05:30",
  "org_id": 42,
  "reference_id": "order-99213",
  "livemode": true,
  "data": {
    "object": "call",
    "unique_id": "run_4821",
    "call_id": "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
    "direction": "outbound",
    "from_number": "07965318581",
    "to_number": "+919812345678",
    "status": "failed",
    "duration_sec": 0,
    "end_reason": "failed",
    "campaign_id": 4821,
    "campaign_type": "audio_blast",
    "reference_id": "order-99213"
  },
  "event": "call.failed",
  "event_id": "evt_d40f5b2e7a9b7d4f1c3e6a8b0d2f4e5a"
}

recording.available

The call recording has finished processing and is ready to download. This is sent after the call ends, once the audio is available. The recording_url is an authenticated API URL; fetch it with your API credentials (see Download a call recording).

data is a recording object.

recording.available
{
  "id": "evt_2c3d4e5f60718293a4b5c6d7e8f9a0b1",
  "object": "event",
  "api_version": "2026-06-01",
  "type": "recording.available",
  "created": 1780589575,
  "occurred_at": "2026-06-04T16:12:55.000Z",
  "timestamp_ist": "2026-06-04T21:42:55.000+05:30",
  "org_id": 42,
  "reference_id": "order-99213",
  "livemode": true,
  "data": {
    "object": "recording",
    "unique_id": "run_4821",
    "call_id": "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
    "recording_url": "https://voice.agentive.co.in/api/public/v1/calls/a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d/recording",
    "format": "ogg",
    "duration_sec": 42,
    "size_bytes": 126976,
    "campaign_id": 4821,
    "campaign_type": "audio_blast",
    "reference_id": "order-99213"
  },
  "event": "recording.available",
  "event_id": "evt_2c3d4e5f60718293a4b5c6d7e8f9a0b1"
}

otp.sent

A voice verification call was queued for delivery. The verification code itself is never included in any webhook.

FieldValue at this event
status"queued"
verifiednull
expires_in_secseconds until the code expires
max_attemptsallowed verify attempts
otp.sent
{
  "id": "evt_e51a6c3f8b0c8e5a2d4f7b9c1e3a5f6b",
  "object": "event",
  "api_version": "2026-06-01",
  "type": "otp.sent",
  "created": 1780590000,
  "occurred_at": "2026-06-04T16:20:00.000Z",
  "timestamp_ist": "2026-06-04T21:50:00.000+05:30",
  "org_id": 42,
  "reference_id": "signup-8841",
  "livemode": true,
  "data": {
    "object": "verification",
    "request_id": "d29b8e7a-3c41-4f0a-9b2d-7e1c5a6f8b90",
    "number": "+919812345678",
    "status": "queued",
    "verified": null,
    "expires_in_sec": 600,
    "max_attempts": 3,
    "campaign_id": 5567,
    "reference_id": "signup-8841"
  },
  "event": "otp.sent",
  "event_id": "evt_e51a6c3f8b0c8e5a2d4f7b9c1e3a5f6b"
}

otp.verified

A recipient entered the correct verification code.

FieldValue at this event
status"verified"
verifiedtrue
otp.verified
{
  "id": "evt_f62b7d4a9c1d9f6b3e5a8c0d2f4b6a7c",
  "object": "event",
  "api_version": "2026-06-01",
  "type": "otp.verified",
  "created": 1780590075,
  "occurred_at": "2026-06-04T16:21:15.000Z",
  "timestamp_ist": "2026-06-04T21:51:15.000+05:30",
  "org_id": 42,
  "reference_id": "signup-8841",
  "livemode": true,
  "data": {
    "object": "verification",
    "request_id": "d29b8e7a-3c41-4f0a-9b2d-7e1c5a6f8b90",
    "number": "+919812345678",
    "status": "verified",
    "verified": true,
    "expires_in_sec": null,
    "max_attempts": null,
    "campaign_id": 5567,
    "reference_id": "signup-8841"
  },
  "event": "otp.verified",
  "event_id": "evt_f62b7d4a9c1d9f6b3e5a8c0d2f4b6a7c"
}

otp.failed

A recipient entered an incorrect code. The verification is still open until it expires or runs out of attempts, so the recipient can try again. The verification code itself is never included.

FieldValue at this event
status"failed"
verifiedfalse
attempts_lefthow many verify attempts remain
max_attemptsallowed verify attempts
otp.failed
{
  "id": "evt_3d4e5f60718293a4b5c6d7e8f9a0b1c2",
  "object": "event",
  "api_version": "2026-06-01",
  "type": "otp.failed",
  "created": 1780590050,
  "occurred_at": "2026-06-04T16:20:50.000Z",
  "timestamp_ist": "2026-06-04T21:50:50.000+05:30",
  "org_id": 42,
  "reference_id": "signup-8841",
  "livemode": true,
  "data": {
    "object": "verification",
    "request_id": "d29b8e7a-3c41-4f0a-9b2d-7e1c5a6f8b90",
    "number": "+919812345678",
    "status": "failed",
    "verified": false,
    "attempts_left": 2,
    "max_attempts": 3,
    "campaign_id": 5567,
    "reference_id": "signup-8841"
  },
  "event": "otp.failed",
  "event_id": "evt_3d4e5f60718293a4b5c6d7e8f9a0b1c2"
}

otp.max_attempts

Too many incorrect attempts. The code is now locked and can no longer be verified. Send a new code to retry. Treat this as a signal that the recipient exhausted their attempts. The verification code itself is never included.

FieldValue at this event
status"max_attempts"
verifiedfalse
attempts_left0
max_attemptsallowed verify attempts
otp.max_attempts
{
  "id": "evt_4e5f60718293a4b5c6d7e8f9a0b1c2d3",
  "object": "event",
  "api_version": "2026-06-01",
  "type": "otp.max_attempts",
  "created": 1780590120,
  "occurred_at": "2026-06-04T16:22:00.000Z",
  "timestamp_ist": "2026-06-04T21:52:00.000+05:30",
  "org_id": 42,
  "reference_id": "signup-8841",
  "livemode": true,
  "data": {
    "object": "verification",
    "request_id": "d29b8e7a-3c41-4f0a-9b2d-7e1c5a6f8b90",
    "number": "+919812345678",
    "status": "max_attempts",
    "verified": false,
    "attempts_left": 0,
    "max_attempts": 3,
    "campaign_id": 5567,
    "reference_id": "signup-8841"
  },
  "event": "otp.max_attempts",
  "event_id": "evt_4e5f60718293a4b5c6d7e8f9a0b1c2d3"
}

The call object

data.object is "call" for every call.* event. The same fields are present on each call event; the values that are known depend on where the call is in its lifecycle.

FieldTypeDescription
objectstringAlways "call".
unique_idstringThe call's public id, matching Get call status. For API-placed calls this looks like run_4821.
call_idstring | nullOur internal identifier for the call. null on call.initiated, present from call.ringing onward.
directionstringoutbound or inbound.
from_numberstringThe caller number shown.
to_numberstringThe recipient number.
statusstringThe call state at this event. One of the enumerated status values (see Status and end reasons).
duration_secnumber | nullTalk duration in seconds. Set on the terminal events (call.completed, call.no_answer, call.busy, call.failed), null before.
end_reasonstring | nullWhy the call ended. One of the enumerated end reasons. Set on the terminal events, null before.
campaign_idnumber | nullThe run this call belongs to, matching the digits in unique_id.
campaign_typestring | nullThe kind of run: audio_blast, press1, or otp. null for calls not part of an API run.
reference_idstring | nullYour correlation key.
Lifecycle
Status values follow the call through its lifecycle: queued (initiated) → ringing → in_progress (answered) → completed, or missed if it rang without being answered, or failed if it could not be placed.

Call status and end reasons

status and end_reason on the call object are closed sets. New values are not added without notice, so you can write exhaustive handling (for example a switch with no open-ended fallback).

status (call state at the current event)

ValueMeaningSeen on
queuedAccepted and waiting to be placed.call.initiated
ringingThe recipient's phone is ringing.call.ringing
in_progressThe recipient answered and the call is connected.call.answered
completedThe call finished after being answered.call.completed
missedThe call rang but was never answered (no answer, busy, or declined).call.no_answer, call.busy
failedThe call could not be placed.call.failed

end_reason (set on the terminal events, null before)

ValueMeaningTerminal event
completedThe call connected and finished normally.call.completed
no_answerThe call rang but the recipient never picked up. Safe to retry later.call.no_answer
busyThe recipient was on another call. The number is reachable, so a retry can succeed.call.busy
rejectedThe recipient actively declined the call.call.busy
canceledThe call was cancelled before it connected.call.failed
failedThe call could not be placed, for example an invalid number or a carrier problem.call.failed

You can branch either on the event type or on end_reason; they are kept in step. The same enumerated values are returned by List calls.

The recording object

data.object is "recording" for the recording.available event.

FieldTypeDescription
objectstringAlways "recording".
unique_idstringThe call's public id, matching Get call status. For API-placed calls this looks like run_4821.
call_idstring | nullOur internal identifier for the call this recording belongs to.
recording_urlstringAn authenticated API URL to download the audio. Fetch it with your API credentials.
formatstringThe audio format: "ogg" or "wav".
duration_secnumber | nullThe recorded length in seconds.
size_bytesnumber | nullThe file size in bytes.
campaign_idnumber | nullThe run this call belongs to, if any.
campaign_typestring | nullThe kind of run: audio_blast, press1, or otp.
reference_idstring | nullYour correlation key.
Downloading the audio
The recording_url points at Download a call recording. Call it with your API key and secret; it streams the audio and supports HTTP Range.

The verification object

data.object is "verification" for otp.sent, otp.verified, otp.failed, and otp.max_attempts.

FieldTypeDescription
objectstringAlways "verification".
request_idstringThe verification request id, matching Get call status.
numberstringThe recipient's number (E.164).
statusstringqueued on otp.sent, verified on otp.verified, failed on otp.failed, max_attempts on otp.max_attempts.
verifiedboolean | nullnull on otp.sent, true on otp.verified, false on otp.failed and otp.max_attempts.
attempts_leftnumber | nullRemaining verify attempts. Present on otp.failed (1 or more) and otp.max_attempts (0).
expires_in_secnumber | nullSeconds until the code expires. Present on otp.sent.
max_attemptsnumber | nullAllowed verify attempts. Present on otp.sent, otp.failed, and otp.max_attempts.
campaign_idnumber | nullThe delivery run, if any.
reference_idstring | nullYour correlation key.
The code is never sent
The verification code is never included in any webhook.

Headers

Each delivery includes these headers:

HeaderDescription
Content-Typeapplication/json
X-Agentive-EventThe event name (type), for quick routing without parsing the body.
X-Agentive-Event-IdThe same id as in the body.
X-Agentive-SignatureThe signature: sha256=<hex digest>. Present when the endpoint has a signing secret.
User-AgentIdentifies the delivery agent.

Verifying the signature

Always verify the signature before trusting a webhook. It proves the request came from us and that the body was not altered. The signature is an HMAC-SHA256 of the exact raw request body using your endpoint's signing secret, hex-encoded, with a sha256= prefix:

Header
X-Agentive-Signature: sha256=4f1d...e9

To verify:

  1. 1
    Read the raw request body as bytes, before any JSON parsing or re-serialising. Re-serialising can change whitespace and break the check.
  2. 2
    Compute HMAC-SHA256(secret, rawBody) and hex-encode it.
  3. 3
    Prefix with sha256=.
  4. 4
    Compare to X-Agentive-Signature using a constant-time comparison.
Reject on mismatch
If the computed signature and the header do not match, reject the request.

Example handlers

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

const app = express();

// Capture the RAW body so the signature is computed over the exact bytes.
app.use("/webhooks/agentive", express.raw({ type: "application/json" }));

app.post("/webhooks/agentive", (req, res) => {
  const secret = process.env.AGENTIVE_WEBHOOK_SECRET;
  const signature = req.header("X-Agentive-Signature") || "";

  const expected =
    "sha256=" +
    crypto.createHmac("sha256", secret).update(req.body).digest("hex");

  const ok =
    signature.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));

  if (!ok) return res.status(401).send("bad signature");

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

  // Idempotency: skip if you have already processed evt.id.
  switch (evt.type) {
    case "call.completed":
      // evt.data is a call object
      console.log("call done", evt.data.unique_id, evt.data.duration_sec);
      break;
    case "otp.verified":
      console.log("verified", evt.data.request_id, evt.reference_id);
      break;
    default:
      // Ignore events you do not handle.
      break;
  }

  res.sendStatus(200);
});

app.listen(3000);

Verifying with curl (manual check)

You can reproduce a signature for a known body and secret to confirm your tooling:

Shell
BODY='{"type":"call.completed","id":"evt_abc","created":1780589528,"data":{}}'
printf '%s' "$BODY" | openssl dgst -sha256 -hmac "your_webhook_secret" -hex
# prepend "sha256=" to the result and compare to X-Agentive-Signature

Responding to a webhook

  • Reply with any 2xx status (200 is fine) once you have safely received the event. We treat 2xx as delivered.
  • Reply quickly. Do the heavy work asynchronously. Each attempt has a 6 second timeout; if your endpoint is slow the attempt is treated as failed and retried.
  • Any non-2xx status, a timeout, or a connection error counts as a failed attempt and triggers a retry.

Retries and delivery

  • We attempt delivery up to 3 times per event.
  • Backoff between attempts is 0.5 s, then 2 s, then 5 s.
  • Each attempt has a 6 second timeout.
  • A delivery succeeds as soon as your endpoint returns a 2xx.
  • Redirects are not followed: a 3xx response is treated as a non-success, so a signed body is never forwarded to another host.
  • If all three attempts fail, the event is dropped (it is not queued for later redelivery). The failure is recorded in the delivery log. To recover a missed event, reconcile from the API: poll GET /calls/{id} for a specific call, or page through GET /calls. Build your system so a missed webhook is not the only way you learn a call's outcome.
  • Every attempt is recorded. The dashboard delivery log shows the event, the final response code (or blank for a network error), the number of attempts, whether it was delivered, and the time in IST. The stored payload is the exact body we sent, so you can replay or debug it.

Ordering and idempotency

  • Receiver-side dedup. The same id (evt_...) is sent to every endpoint that receives a given event, and is repeated across retries of that delivery. Treat an event as already handled if you have seen its id before.
  • Ordering is not guaranteed. Events may arrive out of order. Under retries, call.answered could arrive after call.completed. Order by the envelope timestamp (created, or occurred_at / timestamp_ist) and the status field, never by arrival order.
  • A call typically produces this sequence: call.initiated (API-placed calls only), then call.ringing (inbound only), then call.answered and call.completed if it connects, or one of call.no_answer, call.busy, or call.failed if it does not, followed by recording.available once the audio is processed. Build your logic around the terminal event (call.completed, call.no_answer, call.busy, or call.failed) rather than the intermediate ones.
  • Request idempotency is your responsibility. The call-placing API endpoints (POST /calls and POST /otp/send) do not yet accept an idempotency key. A network retry of one of those requests may place a second call. Send a unique reference_id and dedupe on your side before retrying. See Idempotency in the API reference.

Testing

From the Webhooks tab:

  • View sample payloads shows the exact body for each event, so you can build and unit-test your handler before any live traffic.
  • Send test event delivers a sample call.completed to a single endpoint. The sample body has the real envelope shape, signed exactly like a live delivery, so you can confirm your receiver and signature verification end to end. To let you tell it apart from real traffic it carries "test": true inside data and a reference_id of test-reference. The dashboard reports the response code and attempts.
No separate test mode
There is no separate test or sandbox mode: livemode is always true, and the only marker on a manually sent test event is the data.test flag described above (never present on real events). A request-bin style URL is a quick way to inspect the raw body, headers, and signature during integration.

Versioning

  • Every event carries api_version. The current version is 2026-06-01.
  • We may add new fields to the envelope or to any data object at any time without changing the version. Your handler must ignore fields it does not recognise.
  • We may add new event types. An endpoint subscribed to "all events" will start receiving them; switch on type and ignore events you do not handle.
  • A removed or renamed field is a breaking change and would come with a new api_version. We would not change the meaning of an existing field in place.

Security checklist

Before you go live
  • Verify X-Agentive-Signature on every request; reject anything that does not match.
  • Compare signatures in constant time.
  • Use the raw request body for the HMAC, not a re-serialised object.
  • Serve your webhook endpoint over HTTPS.
  • Keep the signing secret server-side; rotate it from the dashboard if it is ever exposed.
  • Make your handler idempotent using id.
  • Return 2xx only after you have durably accepted the event.
Related
See the API Reference for placing calls, sending verification codes, and checking status. Timestamps are in IST; call charges are in INR.