Events, webhooks and debugging
Contactivity tells your server what happens on calls through webhooks, keeps a 7-day event log, and records
every request it sends you and every error it hits. Paths are relative to
https://api.contactivity.io/v1/projects/{project_id}; see the overview.
| Method | Path | Scope | Purpose |
|---|---|---|---|
| GET | /events |
events:read |
Event log (paged) |
| GET | /webhook-endpoint |
events:read |
The project's default webhook endpoint |
| PUT | /webhook-endpoint |
events:write |
Set or turn off the endpoint |
| GET | /webhook-public-key |
any key | The Ed25519 key that signs webhooks (trunks-routes.md) |
| POST | /webhook-public-key/rotate |
events:write |
New signing key; the old one keeps signing for 24 h |
| GET | /webhook-deliveries |
events:read |
Every request sent to your URLs (paged) |
| GET | /errors |
events:read |
Error debugger (paged) |
| GET | /alert-rules |
events:read |
List alert rules (paged) |
| POST | /alert-rules |
events:write |
Create an alert rule |
| PATCH | /alert-rules/{rule_id} |
events:write |
Update an alert rule |
| DELETE | /alert-rules/{rule_id} |
events:write |
Archive an alert rule |
Native events
Voice apps with param_style: "contactivity" (the default for apps created through this API, see
voice-apps.md) send call-control events in the native envelope, and recording and
transcription events use it too:
{
"id": "evt_5f0c…",
"type": "call.answered",
"created_at": "2026-10-22T14:03:11.123Z",
"project_id": "6a1f…",
"data": {"call_id": "CA…", "parent_call_id": null, "app_id": "app_us1_…", "client_state": "b3JkZXItNDI=",
"from": "+12125550123", "to": "+14155550100", "direction": "outbound"}
}data always has call_id, parent_call_id, app_id and client_state; from, to, direction and
custom_parameters when known; plus fields of the event (digits, recording ids, ...).
Event types:
| Group | Types |
|---|---|
| Call lifecycle | call.initiated, call.answered, call.bridged, call.completed, call.error |
| Input | call.dtmf.received, call.gather.ended, call.ai_gather.ended |
| Media | call.playback.started, call.playback.ended, call.speak.started, call.speak.ended |
| Queues and transfer | call.enqueued, call.dequeued, call.refer.started, call.refer.completed, call.refer.failed |
| Machine detection | call.machine_detection.ended, call.machine_greeting.ended |
| Streams and live transcription | call.stream.started, call.stream.stopped, call.stream.failed, call.transcription.received |
| AI agents | call.ai_agent.started, call.ai_agent.ended |
| Recordings and transcriptions | call.recording.completed, call.transcription.completed, call.transcription.failed |
| Conferences | conference.created, conference.ended, conference.participant.joined, conference.participant.left, conference.playback.started, conference.playback.ended, conference.speak.started, conference.speak.ended, conference.recording.completed |
New types may be added; ignore types you do not handle. Apps with param_style: "twilio" keep the
Twilio/Telnyx-compatible formats (compatibility.md).
Where events go
To the voice app's webhook_url (or the call's webhook_url). Events without a URL of their own go to the
project's webhook endpoint, when one is set:
curl -X PUT https://api.contactivity.io/v1/projects/$PROJECT_ID/webhook-endpoint \
-H "Authorization: Bearer $CONTACTIVITY_API_KEY" -H "Content-Type: application/json" \
-d '{"url": "https://example.com/contactivity-events", "event_types": ["call.completed", "call.recording.completed"]}'url must be a public https:// URL; null or "" turns the endpoint off. event_types empty or missing means
all types. Both GET and PUT return {url, event_types, updated_at}. Errors: 400 invalid_parameter (param
url, event_types), 400 unknown_parameter.
Verifying signatures
Every webhook, CoXML fetch and AI tool call is signed with Standard Webhooks headers: webhook-id (the event
id for native events), webhook-timestamp (Unix seconds) and webhook-signature (v1a,<base64 Ed25519 signature> over {webhook-id}.{webhook-timestamp}.{raw body}). Get the public key from
GET /webhook-public-key, reject timestamps more than 5 minutes old, and verify over the raw body. For 24 h
after a key rotation the header carries two space-separated entries (new key first); a signature is valid when
any entry verifies, which the SDK helpers do:
import { verifyStandardWebhook } from "@contactivity/sdk";
if (!verifyStandardWebhook(publicKey, req.headers, rawBody)) return res.sendStatus(401);from contactivity_sdk import verify_standard_webhook
if not verify_standard_webhook(public_key, request.headers, raw_body):
abort(401)Answer with a 2xx within a few seconds; failed deliveries are retried and show up in /webhook-deliveries.
Event log
GET /events lists the stored events, newest first. The stored copy keeps metadata only (ids, states, causes, times, durations, numbers and media facts); everything else, including transcripts, speech and gather results, digits, AI message history, summaries, client_state and custom parameters, is dropped and the event is marked content_redacted: true. Webhook deliveries carry the full data. Stored events are never deleted: archiving a call, recording or transcription keeps them. Filters: type (one type, or a prefix such as
call.*), call_id, created_after, created_before (RFC 3339 or YYYY-MM-DD), limit, starting_after.
Each item is the envelope above. GET /event-types lists every event type (any API key of the project).
curl "https://api.contactivity.io/v1/projects/$PROJECT_ID/events?type=call.*&call_id=$CALL_ID" \
-H "Authorization: Bearer $CONTACTIVITY_API_KEY"Webhook deliveries
GET /webhook-deliveries lists every request Contactivity sent to your URLs, newest first. Filters: status
(succeeded = 2xx, failed = other status, connection error or timeout), kind (webhook, coxml, alert),
url (exact), call_id, limit, starting_after.
| Field | Type | Description |
|---|---|---|
id |
string | |
kind |
string | webhook, coxml or alert |
event_id |
string | |
call_id |
string or null | |
attempt |
integer | |
method, url |
string | |
status_code |
integer or null | Null on a connection error or timeout |
error |
string or null | |
duration_ms |
integer or null | |
replay_of |
string or null | The delivery this one replayed |
created_at |
timestamp |
Error debugger
GET /errors lists errors raised while running calls and apps (bad CoXML, failed webhooks, AI agent errors),
newest first. Filters: category (coxml, webhook, call, ai_agent, app), call_id, limit,
starting_after. Each item: id, created_at, category, code (integer), message, url, call_id.
Alert rules
An alert rule emails people and/or calls a webhook when errors (or an AI agent metric) cross a threshold within a time window, then waits out a cooldown. A project has at most 20 rules.
| Method | Path | Scope | Purpose |
|---|---|---|---|
| GET | /alert-rules |
events:read |
List rules (paged, newest first) |
| POST | /alert-rules |
events:write |
Create a rule |
| PATCH | /alert-rules/{rule_id} |
events:write |
Update a rule (fields left out keep their values) |
| DELETE | /alert-rules/{rule_id} |
events:write |
Archive a rule (it stops firing) |
| Field | Type | Rules |
|---|---|---|
name |
string | Required. 1-100 characters, trimmed |
category |
string | Required: any, coxml, webhook, call, ai_agent or app (the /errors categories) |
codes |
integer[] | Up to 50 error codes (1-99999); empty counts every code of the category |
metric |
string | errors (default), ai_success_rate, ai_latency_p95 or ai_negative_pct |
threshold |
integer | Required. errors: 1-100000 errors in the window. ai_latency_p95: 50-60000 ms. ai_success_rate, ai_negative_pct: a percentage, 1-100 |
window_minutes |
integer | 1-1440, default 5 |
cooldown_minutes |
integer | 1-10080, default 60 |
emails |
string[] | Up to 5 addresses |
webhook_url |
string or null | A public https:// URL; "" or null clears it |
enabled |
boolean | Default true |
voice_app_id |
string or null | AI metrics only: limit the rule to one AI agent. Ignored for errors |
At least one of emails or webhook_url is required. The rule object has these fields plus id (alr_…),
last_fired_at and created_at.
Errors: 400 invalid_parameter (param name, category, codes, threshold, metric, window_minutes,
cooldown_minutes, emails, webhook_url, voice_app_id), 400 invalid_request (no emails and no webhook_url,
or an AI metric threshold out of range), 400 unknown_parameter, 404 not_found, 409 conflict (already 20 rules).
curl https://api.contactivity.io/v1/projects/$PROJECT_ID/alert-rules \
-H "Authorization: Bearer $CONTACTIVITY_API_KEY" \
-H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
-d '{"name": "Webhook failures", "category": "webhook", "threshold": 10, "window_minutes": 5,
"emails": ["oncall@example.com"]}'
