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.
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).
2026-06-01, sent as api_version on every event. See Versioning for our compatibility promise.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.
Use the Webhooks tab in your dashboard to:
https:// URL and either supply your own signing secret or let one be generated.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.
https:// address. Private, loopback, and internal addresses are rejected when you save the endpoint.There are twelve events, grouped by the resource they describe.
Each carries a call object.
| Event | Object | Fires when |
|---|---|---|
| call.initiated | call | A call you placed through the API is accepted and queued. |
| call.ringing | call | The recipient's phone starts ringing. Inbound calls only (see the note in call.ringing). |
| call.answered | call | The recipient answers and the call connects. |
| call.completed | call | A call that was answered finishes. Carries the talk duration. |
| call.no_answer | call | The call rang but the recipient never picked up. Safe to retry later. |
| call.busy | call | The recipient was on another call or declined. The number is reachable, so a retry can succeed. |
| call.failed | call | The call could not be placed at all (an invalid number or a carrier problem). Retrying the same number is unlikely to help. |
Carries a recording object.
| Event | Object | Fires when |
|---|---|---|
| recording.available | recording | The call recording has finished processing and is ready to download. Sent after the call ends. |
Each carries a verification object.
| Event | Object | Fires when |
|---|---|---|
| otp.sent | verification | A voice verification call is queued for delivery. |
| otp.verified | verification | A verification code is confirmed correct. |
| otp.failed | verification | A recipient entered an incorrect code. The verification is still open until it expires or runs out of attempts. |
| otp.max_attempts | verification | Too 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.
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.Every webhook body is a single JSON object with the same outer envelope. Only the data block changes between events.
{
"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"
}| Field | Type | Description |
|---|---|---|
| id | string | Unique id for this event, prefixed evt_. Use it to make your handler idempotent. |
| object | string | Always "event". |
| api_version | string | The payload version used to build this event. |
| type | string | The event name in resource.action form. |
| created | number | When the event was generated, as a Unix epoch timestamp in seconds. |
| occurred_at | string | The same instant in UTC, ISO 8601 (Z). |
| timestamp_ist | string | The same instant in IST, ISO 8601 with a +05:30 offset. |
| org_id | number | Your account id. |
| reference_id | string | null | The reference_id you supplied when you placed the call or sent the code. null if none. |
| livemode | boolean | true for real events. |
| data | object | The event-specific record. Its object field names the resource type (call or verification). |
| event | string | Alias of type. Kept for compatibility. |
| event_id | string | Alias of id. Kept for compatibility. |
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.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.
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.
| Field | Value at this event |
|---|---|
| status | "queued" |
| call_id | null (assigned at call.ringing) |
| duration_sec | null |
| end_reason | null |
{
"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"
}The recipient's phone has started ringing. From this event onward call_id is present.
call.ringing, because the outbound leg has no reliable ringing signal. Those run call.initiated then call.answered then call.completed (or call.failed).| Field | Value at this event |
|---|---|
| status | "ringing" |
| call_id | present |
| duration_sec | null |
{
"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"
}The recipient picked up and the call is now connected.
| Field | Value at this event |
|---|---|
| status | "in_progress" |
| duration_sec | null (set on completion) |
{
"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"
}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.
| Field | Value at this event |
|---|---|
| status | "completed" |
| duration_sec | talk seconds (an integer, may be 0) |
| end_reason | a short reason, for example "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"
}The call rang but the recipient never picked up. Safe to retry later. This is a terminal event for the call.
| Field | Value at this event |
|---|---|
| status | "missed" |
| duration_sec | 0 |
| end_reason | "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"
}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.
| Field | Value at this event |
|---|---|
| status | "missed" |
| duration_sec | 0 |
| end_reason | "busy" (or "rejected") |
{
"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"
}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.
| Field | Value at this event |
|---|---|
| status | "failed" |
| duration_sec | 0 |
| end_reason | "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"
}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.
{
"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"
}A voice verification call was queued for delivery. The verification code itself is never included in any webhook.
| Field | Value at this event |
|---|---|
| status | "queued" |
| verified | null |
| expires_in_sec | seconds until the code expires |
| max_attempts | allowed verify attempts |
{
"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"
}A recipient entered the correct verification code.
| Field | Value at this event |
|---|---|
| status | "verified" |
| verified | true |
{
"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"
}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.
| Field | Value at this event |
|---|---|
| status | "failed" |
| verified | false |
| attempts_left | how many verify attempts remain |
| max_attempts | allowed verify attempts |
{
"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"
}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.
| Field | Value at this event |
|---|---|
| status | "max_attempts" |
| verified | false |
| attempts_left | 0 |
| max_attempts | allowed verify 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"
}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.
| Field | Type | Description |
|---|---|---|
| object | string | Always "call". |
| unique_id | string | The call's public id, matching Get call status. For API-placed calls this looks like run_4821. |
| call_id | string | null | Our internal identifier for the call. null on call.initiated, present from call.ringing onward. |
| direction | string | outbound or inbound. |
| from_number | string | The caller number shown. |
| to_number | string | The recipient number. |
| status | string | The call state at this event. One of the enumerated status values (see Status and end reasons). |
| duration_sec | number | null | Talk duration in seconds. Set on the terminal events (call.completed, call.no_answer, call.busy, call.failed), null before. |
| end_reason | string | null | Why the call ended. One of the enumerated end reasons. Set on the terminal events, null before. |
| campaign_id | number | null | The run this call belongs to, matching the digits in unique_id. |
| campaign_type | string | null | The kind of run: audio_blast, press1, or otp. null for calls not part of an API run. |
| reference_id | string | null | Your correlation key. |
queued (initiated) → ringing → in_progress (answered) → completed, or missed if it rang without being answered, or failed if it could not be placed.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).
| Value | Meaning | Seen on |
|---|---|---|
| queued | Accepted and waiting to be placed. | call.initiated |
| ringing | The recipient's phone is ringing. | call.ringing |
| in_progress | The recipient answered and the call is connected. | call.answered |
| completed | The call finished after being answered. | call.completed |
| missed | The call rang but was never answered (no answer, busy, or declined). | call.no_answer, call.busy |
| failed | The call could not be placed. | call.failed |
| Value | Meaning | Terminal event |
|---|---|---|
| completed | The call connected and finished normally. | call.completed |
| no_answer | The call rang but the recipient never picked up. Safe to retry later. | call.no_answer |
| busy | The recipient was on another call. The number is reachable, so a retry can succeed. | call.busy |
| rejected | The recipient actively declined the call. | call.busy |
| canceled | The call was cancelled before it connected. | call.failed |
| failed | The 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.
data.object is "recording" for the recording.available event.
| Field | Type | Description |
|---|---|---|
| object | string | Always "recording". |
| unique_id | string | The call's public id, matching Get call status. For API-placed calls this looks like run_4821. |
| call_id | string | null | Our internal identifier for the call this recording belongs to. |
| recording_url | string | An authenticated API URL to download the audio. Fetch it with your API credentials. |
| format | string | The audio format: "ogg" or "wav". |
| duration_sec | number | null | The recorded length in seconds. |
| size_bytes | number | null | The file size in bytes. |
| campaign_id | number | null | The run this call belongs to, if any. |
| campaign_type | string | null | The kind of run: audio_blast, press1, or otp. |
| reference_id | string | null | Your correlation key. |
recording_url points at Download a call recording. Call it with your API key and secret; it streams the audio and supports HTTP Range.data.object is "verification" for otp.sent, otp.verified, otp.failed, and otp.max_attempts.
| Field | Type | Description |
|---|---|---|
| object | string | Always "verification". |
| request_id | string | The verification request id, matching Get call status. |
| number | string | The recipient's number (E.164). |
| status | string | queued on otp.sent, verified on otp.verified, failed on otp.failed, max_attempts on otp.max_attempts. |
| verified | boolean | null | null on otp.sent, true on otp.verified, false on otp.failed and otp.max_attempts. |
| attempts_left | number | null | Remaining verify attempts. Present on otp.failed (1 or more) and otp.max_attempts (0). |
| expires_in_sec | number | null | Seconds until the code expires. Present on otp.sent. |
| max_attempts | number | null | Allowed verify attempts. Present on otp.sent, otp.failed, and otp.max_attempts. |
| campaign_id | number | null | The delivery run, if any. |
| reference_id | string | null | Your correlation key. |
Each delivery includes these headers:
| Header | Description |
|---|---|
| Content-Type | application/json |
| X-Agentive-Event | The event name (type), for quick routing without parsing the body. |
| X-Agentive-Event-Id | The same id as in the body. |
| X-Agentive-Signature | The signature: sha256=<hex digest>. Present when the endpoint has a signing secret. |
| User-Agent | Identifies the delivery agent. |
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:
X-Agentive-Signature: sha256=4f1d...e9To verify:
HMAC-SHA256(secret, rawBody) and hex-encode it.sha256=.X-Agentive-Signature using a constant-time comparison.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);You can reproduce a signature for a known body and secret to confirm your tooling:
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-Signature3xx response is treated as a non-success, so a signed body is never forwarded to another host.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.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.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.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.From the Webhooks tab:
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.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.api_version. The current version is 2026-06-01.data object at any time without changing the version. Your handler must ignore fields it does not recognise.type and ignore events you do not handle.api_version. We would not change the meaning of an existing field in place.X-Agentive-Signature on every request; reject anything that does not match.id.