Webhook endpoints
Where we send events, and how each delivery went.
Every example sends your key ID and secret as CALLVIEW_KEY_ID and CALLVIEW_SECRET (see Keys and authentication). The base address is https://v2-api.callview.ai/api/v1/external.
Create a webhook endpoint
Section titled “Create a webhook endpoint”POST /webhook_endpoints
Where we send events. HTTPS only, and the address must be on the public internet. The endpoint’s mode is the key’s: a test key’s endpoint gets test events only. The secret (whsec_…) is in this reply only. Every delivery is signed with it in the CallView-Signature header (see the Webhooks guide). At most 10 endpoints per organization. Needs webhooks:write, plus the read permission of every event’s data: leads:read for lead.created, lead.queued, lead.opted_out, lead.late and callback.booked; calls:read for call.*, lead.interested, handover.accepted and voicemail.left (403 permission_denied names the one missing).
Parameters
| Name | In | Type | Required | Notes |
|---|---|---|---|---|
Idempotency-Key |
header | string | Any string you choose (1 to 255 visible characters), new for each new action. Send the same key again within 24 hours and you get the first answer back instead of a second lead or call. At most 255 characters. |
Body
| Field | Type | Required | Notes |
|---|---|---|---|
url |
string | yes | At most 2048 characters. |
events |
array of string | yes | At most 50 items. |
campaign_ids |
array of string (uuid) or null | Only these campaigns. null or left out = every campaign (needs a key for every campaign). |
curl -X POST 'https://v2-api.callview.ai/api/v1/external/webhook_endpoints' \ -u "$CALLVIEW_KEY_ID:$CALLVIEW_SECRET" \ -H 'Idempotency-Key: postWebhookEndpoints-0001' \ -H 'Content-Type: application/json' \ -d '{ "url": "https://crm.example.com/callview", "events": [ "call.completed", "lead.interested" ]}'import { randomUUID } from 'node:crypto';
const auth = Buffer.from(`${process.env.CALLVIEW_KEY_ID}:${process.env.CALLVIEW_SECRET}`).toString('base64');
const res = await fetch('https://v2-api.callview.ai/api/v1/external/webhook_endpoints', { method: 'POST', headers: { Authorization: `Basic ${auth}`, 'Content-Type': 'application/json', 'Idempotency-Key': randomUUID() }, body: JSON.stringify({ "url": "https://crm.example.com/callview", "events": [ "call.completed", "lead.interested" ] }),});console.log(res.status, res.headers.get('request-id'));console.log(await res.text());import os, uuid
import requests
res = requests.request( "POST", "https://v2-api.callview.ai/api/v1/external/webhook_endpoints", auth=(os.environ["CALLVIEW_KEY_ID"], os.environ["CALLVIEW_SECRET"]), headers={"Idempotency-Key": str(uuid.uuid4())}, json={ "url": "https://crm.example.com/callview", "events": [ "call.completed", "lead.interested", ], }, timeout=30,)print(res.status_code, res.headers.get("Request-Id"))print(res.text)<?php$ch = curl_init('https://v2-api.callview.ai/api/v1/external/webhook_endpoints');curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => 'POST', CURLOPT_USERPWD => getenv('CALLVIEW_KEY_ID') . ':' . getenv('CALLVIEW_SECRET'), CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Idempotency-Key: ' . bin2hex(random_bytes(16))], CURLOPT_POSTFIELDS => <<<'JSON'{ "url": "https://crm.example.com/callview", "events": [ "call.completed", "lead.interested" ]}JSON, CURLOPT_HEADER => false,]);$body = curl_exec($ch);echo curl_getinfo($ch, CURLINFO_HTTP_CODE), "\n", $body, "\n";201 Created. Keep the secret; it is not shown again.
{ "data": { "id": "3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b", "object": "webhook_endpoint", "url": "https://crm.example.com/callview", "events": [ "call.completed", "lead.interested" ], "campaign_ids": null, "mode": "live", "livemode": true, "status": "active", "failing_since": null, "paused_at": null, "last_success_at": "2026-10-06T17:05:21.000Z", "previous_secret_expires_at": null, "created": "2026-10-01T09:00:00.000Z", "updated": "2026-10-01T09:00:00.000Z", "secret": "whsec_DO_NOT_USE_this_is_an_example_value" }}Errors
| Status | Codes |
|---|---|
| 400 | invalid_request |
| 401 | authentication_failed |
| 403 | permission_denied, limit_reached |
| 404 | not_found |
| 409 | idempotency_mismatch, idempotency_in_progress |
| 429 | rate_limited, too_many_concurrent_requests |
| 500 | internal_error |
| 503 | service_unavailable |
List webhook endpoints
Section titled “List webhook endpoints”GET /webhook_endpoints
The endpoints for the key’s mode (test or live) within its campaigns. Needs webhooks:read.
curl -X GET 'https://v2-api.callview.ai/api/v1/external/webhook_endpoints' \ -u "$CALLVIEW_KEY_ID:$CALLVIEW_SECRET"const auth = Buffer.from(`${process.env.CALLVIEW_KEY_ID}:${process.env.CALLVIEW_SECRET}`).toString('base64');
const res = await fetch('https://v2-api.callview.ai/api/v1/external/webhook_endpoints', { method: 'GET', headers: { Authorization: `Basic ${auth}` },});console.log(res.status, res.headers.get('request-id'));console.log(await res.text());import os
import requests
res = requests.request( "GET", "https://v2-api.callview.ai/api/v1/external/webhook_endpoints", auth=(os.environ["CALLVIEW_KEY_ID"], os.environ["CALLVIEW_SECRET"]), timeout=30,)print(res.status_code, res.headers.get("Request-Id"))print(res.text)<?php$ch = curl_init('https://v2-api.callview.ai/api/v1/external/webhook_endpoints');curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => 'GET', CURLOPT_USERPWD => getenv('CALLVIEW_KEY_ID') . ':' . getenv('CALLVIEW_SECRET'), CURLOPT_RETURNTRANSFER => true, CURLOPT_HEADER => false,]);$body = curl_exec($ch);echo curl_getinfo($ch, CURLINFO_HTTP_CODE), "\n", $body, "\n";200 Your endpoints
{ "data": [ { "id": "3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b", "object": "webhook_endpoint", "url": "https://crm.example.com/callview", "events": [ "call.completed", "lead.interested" ], "campaign_ids": null, "mode": "live", "livemode": true, "status": "active", "failing_since": null, "paused_at": null, "last_success_at": "2026-10-06T17:05:21.000Z", "previous_secret_expires_at": null, "created": "2026-10-01T09:00:00.000Z", "updated": "2026-10-01T09:00:00.000Z" } ], "meta": { "next_cursor": null, "has_more": false }}Errors
| Status | Codes |
|---|---|
| 400 | invalid_request |
| 401 | authentication_failed |
| 403 | permission_denied |
| 429 | rate_limited, too_many_concurrent_requests |
| 500 | internal_error |
Get a webhook endpoint
Section titled “Get a webhook endpoint”GET /webhook_endpoints/{id}
Needs webhooks:read. Another organization’s endpoint, or one of the other mode, is 404.
Parameters
| Name | In | Type | Required | Notes |
|---|---|---|---|---|
id |
path | string (uuid) | yes |
curl -X GET 'https://v2-api.callview.ai/api/v1/external/webhook_endpoints/3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b' \ -u "$CALLVIEW_KEY_ID:$CALLVIEW_SECRET"const auth = Buffer.from(`${process.env.CALLVIEW_KEY_ID}:${process.env.CALLVIEW_SECRET}`).toString('base64');
const res = await fetch('https://v2-api.callview.ai/api/v1/external/webhook_endpoints/3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b', { method: 'GET', headers: { Authorization: `Basic ${auth}` },});console.log(res.status, res.headers.get('request-id'));console.log(await res.text());import os
import requests
res = requests.request( "GET", "https://v2-api.callview.ai/api/v1/external/webhook_endpoints/3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b", auth=(os.environ["CALLVIEW_KEY_ID"], os.environ["CALLVIEW_SECRET"]), timeout=30,)print(res.status_code, res.headers.get("Request-Id"))print(res.text)<?php$ch = curl_init('https://v2-api.callview.ai/api/v1/external/webhook_endpoints/3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b');curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => 'GET', CURLOPT_USERPWD => getenv('CALLVIEW_KEY_ID') . ':' . getenv('CALLVIEW_SECRET'), CURLOPT_RETURNTRANSFER => true, CURLOPT_HEADER => false,]);$body = curl_exec($ch);echo curl_getinfo($ch, CURLINFO_HTTP_CODE), "\n", $body, "\n";200 The endpoint (never its secret)
{ "data": { "id": "3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b", "object": "webhook_endpoint", "url": "https://crm.example.com/callview", "events": [ "call.completed", "lead.interested" ], "campaign_ids": null, "mode": "live", "livemode": true, "status": "active", "failing_since": null, "paused_at": null, "last_success_at": "2026-10-06T17:05:21.000Z", "previous_secret_expires_at": null, "created": "2026-10-01T09:00:00.000Z", "updated": "2026-10-01T09:00:00.000Z" }}Errors
| Status | Codes |
|---|---|
| 400 | invalid_request |
| 401 | authentication_failed |
| 403 | permission_denied |
| 404 | not_found |
| 429 | rate_limited, too_many_concurrent_requests |
| 500 | internal_error |
Change a webhook endpoint
Section titled “Change a webhook endpoint”PATCH /webhook_endpoints/{id}
url, events, campaign_ids (null = every campaign) or status (active or paused). A new address is checked like a new endpoint’s. Needs webhooks:write; changing url, events or campaign_ids also needs the read permission of every event the endpoint takes (as at creation).
Parameters
| Name | In | Type | Required | Notes |
|---|---|---|---|---|
id |
path | string (uuid) | yes | |
Idempotency-Key |
header | string | Any string you choose (1 to 255 visible characters), new for each new action. Send the same key again within 24 hours and you get the first answer back instead of a second lead or call. At most 255 characters. |
Body
| Field | Type | Required | Notes |
|---|---|---|---|
url |
string | At most 2048 characters. | |
events |
array of string | At most 50 items. | |
campaign_ids |
array of string (uuid) or null | ||
status |
string | One of active / paused. |
curl -X PATCH 'https://v2-api.callview.ai/api/v1/external/webhook_endpoints/3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b' \ -u "$CALLVIEW_KEY_ID:$CALLVIEW_SECRET" \ -H 'Idempotency-Key: patchWebhookEndpointsId-0001' \ -H 'Content-Type: application/json' \ -d '{ "events": [ "call.completed", "lead.interested", "lead.opted_out" ]}'import { randomUUID } from 'node:crypto';
const auth = Buffer.from(`${process.env.CALLVIEW_KEY_ID}:${process.env.CALLVIEW_SECRET}`).toString('base64');
const res = await fetch('https://v2-api.callview.ai/api/v1/external/webhook_endpoints/3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b', { method: 'PATCH', headers: { Authorization: `Basic ${auth}`, 'Content-Type': 'application/json', 'Idempotency-Key': randomUUID() }, body: JSON.stringify({ "events": [ "call.completed", "lead.interested", "lead.opted_out" ] }),});console.log(res.status, res.headers.get('request-id'));console.log(await res.text());import os, uuid
import requests
res = requests.request( "PATCH", "https://v2-api.callview.ai/api/v1/external/webhook_endpoints/3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b", auth=(os.environ["CALLVIEW_KEY_ID"], os.environ["CALLVIEW_SECRET"]), headers={"Idempotency-Key": str(uuid.uuid4())}, json={ "events": [ "call.completed", "lead.interested", "lead.opted_out", ], }, timeout=30,)print(res.status_code, res.headers.get("Request-Id"))print(res.text)<?php$ch = curl_init('https://v2-api.callview.ai/api/v1/external/webhook_endpoints/3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b');curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => 'PATCH', CURLOPT_USERPWD => getenv('CALLVIEW_KEY_ID') . ':' . getenv('CALLVIEW_SECRET'), CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Idempotency-Key: ' . bin2hex(random_bytes(16))], CURLOPT_POSTFIELDS => <<<'JSON'{ "events": [ "call.completed", "lead.interested", "lead.opted_out" ]}JSON, CURLOPT_HEADER => false,]);$body = curl_exec($ch);echo curl_getinfo($ch, CURLINFO_HTTP_CODE), "\n", $body, "\n";200 The changed endpoint
{ "data": { "id": "3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b", "object": "webhook_endpoint", "url": "https://crm.example.com/callview", "events": [ "call.completed", "lead.interested" ], "campaign_ids": null, "mode": "live", "livemode": true, "status": "active", "failing_since": null, "paused_at": null, "last_success_at": "2026-10-06T17:05:21.000Z", "previous_secret_expires_at": null, "created": "2026-10-01T09:00:00.000Z", "updated": "2026-10-01T09:00:00.000Z" }}Errors
| Status | Codes |
|---|---|
| 400 | invalid_request |
| 401 | authentication_failed |
| 403 | permission_denied |
| 404 | not_found |
| 409 | idempotency_mismatch, idempotency_in_progress |
| 429 | rate_limited, too_many_concurrent_requests |
| 500 | internal_error |
| 503 | service_unavailable |
Delete a webhook endpoint
Section titled “Delete a webhook endpoint”DELETE /webhook_endpoints/{id}
The endpoint and its delivery history go. Events stay in GET /events. Needs webhooks:write.
Parameters
| Name | In | Type | Required | Notes |
|---|---|---|---|---|
id |
path | string (uuid) | yes |
curl -X DELETE 'https://v2-api.callview.ai/api/v1/external/webhook_endpoints/3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b' \ -u "$CALLVIEW_KEY_ID:$CALLVIEW_SECRET"const auth = Buffer.from(`${process.env.CALLVIEW_KEY_ID}:${process.env.CALLVIEW_SECRET}`).toString('base64');
const res = await fetch('https://v2-api.callview.ai/api/v1/external/webhook_endpoints/3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b', { method: 'DELETE', headers: { Authorization: `Basic ${auth}` },});console.log(res.status, res.headers.get('request-id'));console.log(await res.text());import os
import requests
res = requests.request( "DELETE", "https://v2-api.callview.ai/api/v1/external/webhook_endpoints/3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b", auth=(os.environ["CALLVIEW_KEY_ID"], os.environ["CALLVIEW_SECRET"]), timeout=30,)print(res.status_code, res.headers.get("Request-Id"))print(res.text)<?php$ch = curl_init('https://v2-api.callview.ai/api/v1/external/webhook_endpoints/3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b');curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => 'DELETE', CURLOPT_USERPWD => getenv('CALLVIEW_KEY_ID') . ':' . getenv('CALLVIEW_SECRET'), CURLOPT_RETURNTRANSFER => true, CURLOPT_HEADER => false,]);$body = curl_exec($ch);echo curl_getinfo($ch, CURLINFO_HTTP_CODE), "\n", $body, "\n";200 Deleted
{ "data": { "id": "string", "object": "string", "deleted": true, "livemode": true }}Errors
| Status | Codes |
|---|---|
| 400 | invalid_request |
| 401 | authentication_failed |
| 403 | permission_denied |
| 404 | not_found |
| 429 | rate_limited, too_many_concurrent_requests |
| 500 | internal_error |
Send a test event
Section titled “Send a test event”POST /webhook_endpoints/{id}/test
Sends one sample of any event type to this endpoint, so you can check your receiver. It is the event’s example in a real envelope with a new evt_ id, livemode false and “sample”: true, signed with this endpoint’s secret just like a real delivery, and retried like one. It shows in the endpoint’s deliveries. It is not an event, so GET /events doesn’t list it. A paused endpoint has to be resumed first. Needs webhooks:write.
Parameters
| Name | In | Type | Required | Notes |
|---|---|---|---|---|
id |
path | string (uuid) | yes | |
Idempotency-Key |
header | string | Any string you choose (1 to 255 visible characters), new for each new action. Send the same key again within 24 hours and you get the first answer back instead of a second lead or call. At most 255 characters. |
Body
| Field | Type | Required | Notes |
|---|---|---|---|
type |
string | yes | An event type, for example call.completed. |
curl -X POST 'https://v2-api.callview.ai/api/v1/external/webhook_endpoints/3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b/test' \ -u "$CALLVIEW_KEY_ID:$CALLVIEW_SECRET" \ -H 'Idempotency-Key: postWebhookEndpointsIdTest-0001' \ -H 'Content-Type: application/json' \ -d '{ "type": "call.completed"}'import { randomUUID } from 'node:crypto';
const auth = Buffer.from(`${process.env.CALLVIEW_KEY_ID}:${process.env.CALLVIEW_SECRET}`).toString('base64');
const res = await fetch('https://v2-api.callview.ai/api/v1/external/webhook_endpoints/3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b/test', { method: 'POST', headers: { Authorization: `Basic ${auth}`, 'Content-Type': 'application/json', 'Idempotency-Key': randomUUID() }, body: JSON.stringify({ "type": "call.completed" }),});console.log(res.status, res.headers.get('request-id'));console.log(await res.text());import os, uuid
import requests
res = requests.request( "POST", "https://v2-api.callview.ai/api/v1/external/webhook_endpoints/3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b/test", auth=(os.environ["CALLVIEW_KEY_ID"], os.environ["CALLVIEW_SECRET"]), headers={"Idempotency-Key": str(uuid.uuid4())}, json={ "type": "call.completed", }, timeout=30,)print(res.status_code, res.headers.get("Request-Id"))print(res.text)<?php$ch = curl_init('https://v2-api.callview.ai/api/v1/external/webhook_endpoints/3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b/test');curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => 'POST', CURLOPT_USERPWD => getenv('CALLVIEW_KEY_ID') . ':' . getenv('CALLVIEW_SECRET'), CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Idempotency-Key: ' . bin2hex(random_bytes(16))], CURLOPT_POSTFIELDS => <<<'JSON'{ "type": "call.completed"}JSON, CURLOPT_HEADER => false,]);$body = curl_exec($ch);echo curl_getinfo($ch, CURLINFO_HTTP_CODE), "\n", $body, "\n";202 Queued. It goes out within a second or two.
{ "data": { "id": "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a", "object": "webhook_delivery", "endpoint_id": "3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b", "event_id": "evt_callcompletedxxxxxxxxxxx", "event_type": "call.completed", "status": "sent", "attempts": 1, "next_attempt_at": null, "last_attempt_at": "2026-10-06T17:05:21.000Z", "response_status": 200, "response_body": "ok", "duration_ms": 182, "resent_from": null, "created": "2026-10-06T17:05:20.000Z", "livemode": true, "event": { "id": "string", "type": "lead.created", "created": "2026-10-06T17:05:20Z", "livemode": true, "api_version": "2026-10-01", "data": { "object": {} }, "sample": true } }}Errors
| Status | Codes |
|---|---|
| 400 | invalid_request |
| 401 | authentication_failed |
| 403 | permission_denied |
| 404 | not_found |
| 409 | endpoint_paused, idempotency_mismatch, idempotency_in_progress |
| 429 | rate_limited, too_many_concurrent_requests |
| 500 | internal_error |
| 503 | service_unavailable |
An endpoint’s deliveries
Section titled “An endpoint’s deliveries”GET /webhook_endpoints/{id}/deliveries
Each delivery is one event sent (or waiting to be sent) to this endpoint: its status, how many tries (7 at most, over 24 hours), your last answer (the first 4 KB) and the times. Newest first, page by page (starting_after = the next_cursor you were given). Needs webhooks:read.
Parameters
| Name | In | Type | Required | Notes |
|---|---|---|---|---|
id |
path | string (uuid) | yes | |
limit |
query | integer | Default 25. 1 to 100. |
|
starting_after |
query | string (uuid) | ||
status |
query | string | One of pending / sent / failed / gave_up / skipped_paused. |
curl -X GET 'https://v2-api.callview.ai/api/v1/external/webhook_endpoints/3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b/deliveries' \ -u "$CALLVIEW_KEY_ID:$CALLVIEW_SECRET"const auth = Buffer.from(`${process.env.CALLVIEW_KEY_ID}:${process.env.CALLVIEW_SECRET}`).toString('base64');
const res = await fetch('https://v2-api.callview.ai/api/v1/external/webhook_endpoints/3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b/deliveries', { method: 'GET', headers: { Authorization: `Basic ${auth}` },});console.log(res.status, res.headers.get('request-id'));console.log(await res.text());import os
import requests
res = requests.request( "GET", "https://v2-api.callview.ai/api/v1/external/webhook_endpoints/3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b/deliveries", auth=(os.environ["CALLVIEW_KEY_ID"], os.environ["CALLVIEW_SECRET"]), timeout=30,)print(res.status_code, res.headers.get("Request-Id"))print(res.text)<?php$ch = curl_init('https://v2-api.callview.ai/api/v1/external/webhook_endpoints/3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b/deliveries');curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => 'GET', CURLOPT_USERPWD => getenv('CALLVIEW_KEY_ID') . ':' . getenv('CALLVIEW_SECRET'), CURLOPT_RETURNTRANSFER => true, CURLOPT_HEADER => false,]);$body = curl_exec($ch);echo curl_getinfo($ch, CURLINFO_HTTP_CODE), "\n", $body, "\n";200 A page of deliveries
{ "data": [ { "id": "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a", "object": "webhook_delivery", "endpoint_id": "3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b", "event_id": "evt_callcompletedxxxxxxxxxxx", "event_type": "call.completed", "status": "sent", "attempts": 1, "next_attempt_at": null, "last_attempt_at": "2026-10-06T17:05:21.000Z", "response_status": 200, "response_body": "ok", "duration_ms": 182, "resent_from": null, "created": "2026-10-06T17:05:20.000Z", "livemode": true } ], "meta": { "next_cursor": null, "has_more": false }}Errors
| Status | Codes |
|---|---|
| 400 | invalid_request |
| 401 | authentication_failed |
| 403 | permission_denied |
| 404 | not_found |
| 429 | rate_limited, too_many_concurrent_requests |
| 500 | internal_error |
Send a delivery again
Section titled “Send a delivery again”POST /webhook_endpoints/{id}/deliveries/{delivery_id}/resend
A new delivery with the same event id (so your side can skip it if it already has it) and a fresh signature. A paused endpoint has to be resumed first. Needs webhooks:write.
Parameters
| Name | In | Type | Required | Notes |
|---|---|---|---|---|
delivery_id |
path | string | yes | |
id |
path | string (uuid) | yes | |
Idempotency-Key |
header | string | Any string you choose (1 to 255 visible characters), new for each new action. Send the same key again within 24 hours and you get the first answer back instead of a second lead or call. At most 255 characters. |
curl -X POST 'https://v2-api.callview.ai/api/v1/external/webhook_endpoints/3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b/deliveries/5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a/resend' \ -u "$CALLVIEW_KEY_ID:$CALLVIEW_SECRET" \ -H 'Idempotency-Key: postWebhookEndpointsIdDeliveriesDeliveryIdResend-0001'import { randomUUID } from 'node:crypto';
const auth = Buffer.from(`${process.env.CALLVIEW_KEY_ID}:${process.env.CALLVIEW_SECRET}`).toString('base64');
const res = await fetch('https://v2-api.callview.ai/api/v1/external/webhook_endpoints/3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b/deliveries/5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a/resend', { method: 'POST', headers: { Authorization: `Basic ${auth}`, 'Idempotency-Key': randomUUID() },});console.log(res.status, res.headers.get('request-id'));console.log(await res.text());import os, uuid
import requests
res = requests.request( "POST", "https://v2-api.callview.ai/api/v1/external/webhook_endpoints/3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b/deliveries/5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a/resend", auth=(os.environ["CALLVIEW_KEY_ID"], os.environ["CALLVIEW_SECRET"]), headers={"Idempotency-Key": str(uuid.uuid4())}, timeout=30,)print(res.status_code, res.headers.get("Request-Id"))print(res.text)<?php$ch = curl_init('https://v2-api.callview.ai/api/v1/external/webhook_endpoints/3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b/deliveries/5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a/resend');curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => 'POST', CURLOPT_USERPWD => getenv('CALLVIEW_KEY_ID') . ':' . getenv('CALLVIEW_SECRET'), CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Idempotency-Key: ' . bin2hex(random_bytes(16))], CURLOPT_HEADER => false,]);$body = curl_exec($ch);echo curl_getinfo($ch, CURLINFO_HTTP_CODE), "\n", $body, "\n";202 Queued. It goes out within a second or two.
{ "data": { "id": "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a", "object": "webhook_delivery", "endpoint_id": "3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b", "event_id": "evt_callcompletedxxxxxxxxxxx", "event_type": "call.completed", "status": "sent", "attempts": 1, "next_attempt_at": null, "last_attempt_at": "2026-10-06T17:05:21.000Z", "response_status": 200, "response_body": "ok", "duration_ms": 182, "resent_from": null, "created": "2026-10-06T17:05:20.000Z", "livemode": true }}Errors
| Status | Codes |
|---|---|
| 400 | invalid_request |
| 401 | authentication_failed |
| 403 | permission_denied |
| 404 | not_found |
| 409 | endpoint_paused, idempotency_mismatch, idempotency_in_progress |
| 429 | rate_limited, too_many_concurrent_requests |
| 500 | internal_error |
| 503 | service_unavailable |
Roll the signing secret
Section titled “Roll the signing secret”POST /webhook_endpoints/{id}/roll_secret
A new secret, shown once. The old one keeps signing too (two v1 values in the header) for grace_hours, so you can switch over without missing anything. Needs webhooks:write.
Parameters
| Name | In | Type | Required | Notes |
|---|---|---|---|---|
id |
path | string (uuid) | yes | |
Idempotency-Key |
header | string | Any string you choose (1 to 255 visible characters), new for each new action. Send the same key again within 24 hours and you get the first answer back instead of a second lead or call. At most 255 characters. |
Body
| Field | Type | Required | Notes |
|---|---|---|---|
grace_hours |
integer | Default 24. 0 to 168. |
curl -X POST 'https://v2-api.callview.ai/api/v1/external/webhook_endpoints/3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b/roll_secret' \ -u "$CALLVIEW_KEY_ID:$CALLVIEW_SECRET" \ -H 'Idempotency-Key: postWebhookEndpointsIdRollSecret-0001' \ -H 'Content-Type: application/json' \ -d '{ "grace_hours": 24}'import { randomUUID } from 'node:crypto';
const auth = Buffer.from(`${process.env.CALLVIEW_KEY_ID}:${process.env.CALLVIEW_SECRET}`).toString('base64');
const res = await fetch('https://v2-api.callview.ai/api/v1/external/webhook_endpoints/3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b/roll_secret', { method: 'POST', headers: { Authorization: `Basic ${auth}`, 'Content-Type': 'application/json', 'Idempotency-Key': randomUUID() }, body: JSON.stringify({ "grace_hours": 24 }),});console.log(res.status, res.headers.get('request-id'));console.log(await res.text());import os, uuid
import requests
res = requests.request( "POST", "https://v2-api.callview.ai/api/v1/external/webhook_endpoints/3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b/roll_secret", auth=(os.environ["CALLVIEW_KEY_ID"], os.environ["CALLVIEW_SECRET"]), headers={"Idempotency-Key": str(uuid.uuid4())}, json={ "grace_hours": 24, }, timeout=30,)print(res.status_code, res.headers.get("Request-Id"))print(res.text)<?php$ch = curl_init('https://v2-api.callview.ai/api/v1/external/webhook_endpoints/3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b/roll_secret');curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => 'POST', CURLOPT_USERPWD => getenv('CALLVIEW_KEY_ID') . ':' . getenv('CALLVIEW_SECRET'), CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Idempotency-Key: ' . bin2hex(random_bytes(16))], CURLOPT_POSTFIELDS => <<<'JSON'{ "grace_hours": 24}JSON, CURLOPT_HEADER => false,]);$body = curl_exec($ch);echo curl_getinfo($ch, CURLINFO_HTTP_CODE), "\n", $body, "\n";200 The endpoint and its new secret
{ "data": { "id": "3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b", "object": "webhook_endpoint", "url": "https://crm.example.com/callview", "events": [ "call.completed", "lead.interested" ], "campaign_ids": null, "mode": "live", "livemode": true, "status": "active", "failing_since": null, "paused_at": null, "last_success_at": "2026-10-06T17:05:21.000Z", "previous_secret_expires_at": null, "created": "2026-10-01T09:00:00.000Z", "updated": "2026-10-01T09:00:00.000Z", "secret": "whsec_DO_NOT_USE_this_is_an_example_value" }}Errors
| Status | Codes |
|---|---|
| 400 | invalid_request |
| 401 | authentication_failed |
| 403 | permission_denied |
| 404 | not_found |
| 409 | idempotency_mismatch, idempotency_in_progress |
| 429 | rate_limited, too_many_concurrent_requests |
| 500 | internal_error |
| 503 | service_unavailable |
Resume a paused endpoint
Section titled “Resume a paused endpoint”POST /webhook_endpoints/{id}/resume
With resend_skipped true, the deliveries skipped while it was paused (up to 1,000, newest first) are sent again with their original event ids. Needs webhooks:write.
Parameters
| Name | In | Type | Required | Notes |
|---|---|---|---|---|
id |
path | string (uuid) | yes | |
Idempotency-Key |
header | string | Any string you choose (1 to 255 visible characters), new for each new action. Send the same key again within 24 hours and you get the first answer back instead of a second lead or call. At most 255 characters. |
Body
| Field | Type | Required | Notes |
|---|---|---|---|
resend_skipped |
boolean | Default false. |
curl -X POST 'https://v2-api.callview.ai/api/v1/external/webhook_endpoints/3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b/resume' \ -u "$CALLVIEW_KEY_ID:$CALLVIEW_SECRET" \ -H 'Idempotency-Key: postWebhookEndpointsIdResume-0001' \ -H 'Content-Type: application/json' \ -d '{ "resend_skipped": true}'import { randomUUID } from 'node:crypto';
const auth = Buffer.from(`${process.env.CALLVIEW_KEY_ID}:${process.env.CALLVIEW_SECRET}`).toString('base64');
const res = await fetch('https://v2-api.callview.ai/api/v1/external/webhook_endpoints/3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b/resume', { method: 'POST', headers: { Authorization: `Basic ${auth}`, 'Content-Type': 'application/json', 'Idempotency-Key': randomUUID() }, body: JSON.stringify({ "resend_skipped": true }),});console.log(res.status, res.headers.get('request-id'));console.log(await res.text());import os, uuid
import requests
res = requests.request( "POST", "https://v2-api.callview.ai/api/v1/external/webhook_endpoints/3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b/resume", auth=(os.environ["CALLVIEW_KEY_ID"], os.environ["CALLVIEW_SECRET"]), headers={"Idempotency-Key": str(uuid.uuid4())}, json={ "resend_skipped": True, }, timeout=30,)print(res.status_code, res.headers.get("Request-Id"))print(res.text)<?php$ch = curl_init('https://v2-api.callview.ai/api/v1/external/webhook_endpoints/3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b/resume');curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => 'POST', CURLOPT_USERPWD => getenv('CALLVIEW_KEY_ID') . ':' . getenv('CALLVIEW_SECRET'), CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Idempotency-Key: ' . bin2hex(random_bytes(16))], CURLOPT_POSTFIELDS => <<<'JSON'{ "resend_skipped": true}JSON, CURLOPT_HEADER => false,]);$body = curl_exec($ch);echo curl_getinfo($ch, CURLINFO_HTTP_CODE), "\n", $body, "\n";200 The endpoint, and how many deliveries were queued again
{ "data": { "id": "3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b", "object": "webhook_endpoint", "url": "https://crm.example.com/callview", "events": [ "call.completed", "lead.interested" ], "campaign_ids": null, "mode": "live", "livemode": true, "status": "active", "failing_since": null, "paused_at": null, "last_success_at": "2026-10-06T17:05:21.000Z", "previous_secret_expires_at": null, "created": "2026-10-01T09:00:00.000Z", "updated": "2026-10-01T09:00:00.000Z", "resent": 1 }}Errors
| Status | Codes |
|---|---|
| 400 | invalid_request |
| 401 | authentication_failed |
| 403 | permission_denied |
| 404 | not_found |
| 409 | idempotency_mismatch, idempotency_in_progress |
| 429 | rate_limited, too_many_concurrent_requests |
| 500 | internal_error |
| 503 | service_unavailable |
List webhooks (old)
Section titled “List webhooks (old)”GET /webhooks Old: still works, use the newer one
Replaced by GET /webhook_endpoints. Still answers its old shape, with a Deprecation header.
curl -X GET 'https://v2-api.callview.ai/api/v1/external/webhooks' \ -u "$CALLVIEW_KEY_ID:$CALLVIEW_SECRET"const auth = Buffer.from(`${process.env.CALLVIEW_KEY_ID}:${process.env.CALLVIEW_SECRET}`).toString('base64');
const res = await fetch('https://v2-api.callview.ai/api/v1/external/webhooks', { method: 'GET', headers: { Authorization: `Basic ${auth}` },});console.log(res.status, res.headers.get('request-id'));console.log(await res.text());import os
import requests
res = requests.request( "GET", "https://v2-api.callview.ai/api/v1/external/webhooks", auth=(os.environ["CALLVIEW_KEY_ID"], os.environ["CALLVIEW_SECRET"]), timeout=30,)print(res.status_code, res.headers.get("Request-Id"))print(res.text)<?php$ch = curl_init('https://v2-api.callview.ai/api/v1/external/webhooks');curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => 'GET', CURLOPT_USERPWD => getenv('CALLVIEW_KEY_ID') . ':' . getenv('CALLVIEW_SECRET'), CURLOPT_RETURNTRANSFER => true, CURLOPT_HEADER => false,]);$body = curl_exec($ch);echo curl_getinfo($ch, CURLINFO_HTTP_CODE), "\n", $body, "\n";200 The old list
{}Errors
| Status | Codes |
|---|---|
| 400 | invalid_request |
| 401 | authentication_failed |
| 403 | permission_denied |
| 429 | rate_limited, too_many_concurrent_requests |
| 500 | internal_error |
Create a webhook (old)
Section titled “Create a webhook (old)”POST /webhooks Old: still works, use the newer one
Replaced by POST /webhook_endpoints. Still answers its old shape, with a Deprecation header. The secret is in this reply only. Needs the read permission of every event’s data, as POST /webhook_endpoints does.
Parameters
| Name | In | Type | Required | Notes |
|---|---|---|---|---|
Idempotency-Key |
header | string | Any string you choose (1 to 255 visible characters), new for each new action. Send the same key again within 24 hours and you get the first answer back instead of a second lead or call. At most 255 characters. |
Body
| Field | Type | Required | Notes |
|---|---|---|---|
url |
string | yes | |
events |
array of string | yes | |
campaignId |
string (uuid) or null | ||
enabled |
boolean | Default true. |
curl -X POST 'https://v2-api.callview.ai/api/v1/external/webhooks' \ -u "$CALLVIEW_KEY_ID:$CALLVIEW_SECRET" \ -H 'Idempotency-Key: postWebhooks-0001' \ -H 'Content-Type: application/json' \ -d '{ "url": "https://crm.example.com/callview", "events": [ "string" ], "campaignId": "e4d3c2b1-a09f-4e8d-9c7b-6a5f4e3d2c1b", "enabled": true}'import { randomUUID } from 'node:crypto';
const auth = Buffer.from(`${process.env.CALLVIEW_KEY_ID}:${process.env.CALLVIEW_SECRET}`).toString('base64');
const res = await fetch('https://v2-api.callview.ai/api/v1/external/webhooks', { method: 'POST', headers: { Authorization: `Basic ${auth}`, 'Content-Type': 'application/json', 'Idempotency-Key': randomUUID() }, body: JSON.stringify({ "url": "https://crm.example.com/callview", "events": [ "string" ], "campaignId": "e4d3c2b1-a09f-4e8d-9c7b-6a5f4e3d2c1b", "enabled": true }),});console.log(res.status, res.headers.get('request-id'));console.log(await res.text());import os, uuid
import requests
res = requests.request( "POST", "https://v2-api.callview.ai/api/v1/external/webhooks", auth=(os.environ["CALLVIEW_KEY_ID"], os.environ["CALLVIEW_SECRET"]), headers={"Idempotency-Key": str(uuid.uuid4())}, json={ "url": "https://crm.example.com/callview", "events": [ "string", ], "campaignId": "e4d3c2b1-a09f-4e8d-9c7b-6a5f4e3d2c1b", "enabled": True, }, timeout=30,)print(res.status_code, res.headers.get("Request-Id"))print(res.text)<?php$ch = curl_init('https://v2-api.callview.ai/api/v1/external/webhooks');curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => 'POST', CURLOPT_USERPWD => getenv('CALLVIEW_KEY_ID') . ':' . getenv('CALLVIEW_SECRET'), CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Idempotency-Key: ' . bin2hex(random_bytes(16))], CURLOPT_POSTFIELDS => <<<'JSON'{ "url": "https://crm.example.com/callview", "events": [ "string" ], "campaignId": "e4d3c2b1-a09f-4e8d-9c7b-6a5f4e3d2c1b", "enabled": true}JSON, CURLOPT_HEADER => false,]);$body = curl_exec($ch);echo curl_getinfo($ch, CURLINFO_HTTP_CODE), "\n", $body, "\n";201 Created
{}Errors
| Status | Codes |
|---|---|
| 400 | invalid_request |
| 401 | authentication_failed |
| 403 | permission_denied, limit_reached |
| 409 | idempotency_mismatch, idempotency_in_progress |
| 429 | rate_limited, too_many_concurrent_requests |
| 500 | internal_error |
| 503 | service_unavailable |
Delete a webhook (old)
Section titled “Delete a webhook (old)”DELETE /webhooks/{id} Old: still works, use the newer one
Replaced by DELETE /webhook_endpoints/{id}.
Parameters
| Name | In | Type | Required | Notes |
|---|---|---|---|---|
id |
path | string (uuid) | yes |
curl -X DELETE 'https://v2-api.callview.ai/api/v1/external/webhooks/3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b' \ -u "$CALLVIEW_KEY_ID:$CALLVIEW_SECRET"const auth = Buffer.from(`${process.env.CALLVIEW_KEY_ID}:${process.env.CALLVIEW_SECRET}`).toString('base64');
const res = await fetch('https://v2-api.callview.ai/api/v1/external/webhooks/3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b', { method: 'DELETE', headers: { Authorization: `Basic ${auth}` },});console.log(res.status, res.headers.get('request-id'));console.log(await res.text());import os
import requests
res = requests.request( "DELETE", "https://v2-api.callview.ai/api/v1/external/webhooks/3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b", auth=(os.environ["CALLVIEW_KEY_ID"], os.environ["CALLVIEW_SECRET"]), timeout=30,)print(res.status_code, res.headers.get("Request-Id"))print(res.text)<?php$ch = curl_init('https://v2-api.callview.ai/api/v1/external/webhooks/3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b');curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => 'DELETE', CURLOPT_USERPWD => getenv('CALLVIEW_KEY_ID') . ':' . getenv('CALLVIEW_SECRET'), CURLOPT_RETURNTRANSFER => true, CURLOPT_HEADER => false,]);$body = curl_exec($ch);echo curl_getinfo($ch, CURLINFO_HTTP_CODE), "\n", $body, "\n";200 Deleted
{}Errors
| Status | Codes |
|---|---|
| 400 | invalid_request |
| 401 | authentication_failed |
| 403 | permission_denied |
| 404 | not_found |
| 429 | rate_limited, too_many_concurrent_requests |
| 500 | internal_error |
A webhook’s deliveries (old)
Section titled “A webhook’s deliveries (old)”GET /webhooks/{id}/deliveries Old: still works, use the newer one
Replaced by GET /webhook_endpoints/{id}/deliveries. A delivery’s payload is left out unless the key can read that event’s data (leads:read or calls:read, or events:read).
Parameters
| Name | In | Type | Required | Notes |
|---|---|---|---|---|
id |
path | string (uuid) | yes | |
limit |
query | integer | Default 25. 1 to 100. |
curl -X GET 'https://v2-api.callview.ai/api/v1/external/webhooks/3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b/deliveries' \ -u "$CALLVIEW_KEY_ID:$CALLVIEW_SECRET"const auth = Buffer.from(`${process.env.CALLVIEW_KEY_ID}:${process.env.CALLVIEW_SECRET}`).toString('base64');
const res = await fetch('https://v2-api.callview.ai/api/v1/external/webhooks/3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b/deliveries', { method: 'GET', headers: { Authorization: `Basic ${auth}` },});console.log(res.status, res.headers.get('request-id'));console.log(await res.text());import os
import requests
res = requests.request( "GET", "https://v2-api.callview.ai/api/v1/external/webhooks/3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b/deliveries", auth=(os.environ["CALLVIEW_KEY_ID"], os.environ["CALLVIEW_SECRET"]), timeout=30,)print(res.status_code, res.headers.get("Request-Id"))print(res.text)<?php$ch = curl_init('https://v2-api.callview.ai/api/v1/external/webhooks/3f6a9c2e-1b7d-4e5f-8a90-1c2d3e4f5a6b/deliveries');curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => 'GET', CURLOPT_USERPWD => getenv('CALLVIEW_KEY_ID') . ':' . getenv('CALLVIEW_SECRET'), CURLOPT_RETURNTRANSFER => true, CURLOPT_HEADER => false,]);$body = curl_exec($ch);echo curl_getinfo($ch, CURLINFO_HTTP_CODE), "\n", $body, "\n";200 The deliveries
{}Errors
| Status | Codes |
|---|---|
| 400 | invalid_request |
| 401 | authentication_failed |
| 403 | permission_denied |
| 404 | not_found |
| 429 | rate_limited, too_many_concurrent_requests |
| 500 | internal_error |
The event catalog (old)
Section titled “The event catalog (old)”GET /webhooks/events Old: still works, use the newer one
Every event type with a description and an example. The guide’s event reference has the same list.
curl -X GET 'https://v2-api.callview.ai/api/v1/external/webhooks/events' \ -u "$CALLVIEW_KEY_ID:$CALLVIEW_SECRET"const auth = Buffer.from(`${process.env.CALLVIEW_KEY_ID}:${process.env.CALLVIEW_SECRET}`).toString('base64');
const res = await fetch('https://v2-api.callview.ai/api/v1/external/webhooks/events', { method: 'GET', headers: { Authorization: `Basic ${auth}` },});console.log(res.status, res.headers.get('request-id'));console.log(await res.text());import os
import requests
res = requests.request( "GET", "https://v2-api.callview.ai/api/v1/external/webhooks/events", auth=(os.environ["CALLVIEW_KEY_ID"], os.environ["CALLVIEW_SECRET"]), timeout=30,)print(res.status_code, res.headers.get("Request-Id"))print(res.text)<?php$ch = curl_init('https://v2-api.callview.ai/api/v1/external/webhooks/events');curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => 'GET', CURLOPT_USERPWD => getenv('CALLVIEW_KEY_ID') . ':' . getenv('CALLVIEW_SECRET'), CURLOPT_RETURNTRANSFER => true, CURLOPT_HEADER => false,]);$body = curl_exec($ch);echo curl_getinfo($ch, CURLINFO_HTTP_CODE), "\n", $body, "\n";200 The catalog
{}Errors
| Status | Codes |
|---|---|
| 400 | invalid_request |
| 401 | authentication_failed |
| 403 | permission_denied |
| 429 | rate_limited, too_many_concurrent_requests |
| 500 | internal_error |