# 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](/guides/overview.md).

| 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](/guides/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](/guides/voice-apps.md)) send call-control events in the native envelope, and recording and
transcription events use it too:

```json
{
  "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](/guides/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:

```sh
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:

```ts
import { verifyStandardWebhook } from "@contactivity/sdk";
if (!verifyStandardWebhook(publicKey, req.headers, rawBody)) return res.sendStatus(401);
```

```python
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).

```sh
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).

```sh
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"]}'
```
