> ## Documentation Index
> Fetch the complete documentation index at: https://docs.traceten.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks API

> Register, test, rotate and inspect outbound webhook endpoints from the API.

## What this covers

Managing the endpoints Traceten sends events to. If you want to know what those events look like or how to verify their signatures, start with the [Webhooks guide](/webhooks/overview).

```
https://api.traceten.com/v1/sites/{siteId}/webhooks
```

`{siteId}` is your snippet key (`ttid_…`) when you authenticate with an API key. Reading needs `stats:read`. Registering a webhook and rotating its secret need `credentials:write`, because both hand back a signing secret. Editing, deleting and testing need `config:write`.

## `GET /v1/sites/{siteId}/webhooks`

```bash theme={null}
curl https://api.traceten.com/v1/sites/ttid_7Rb4TrC1dTbnD8w3s1TS12/webhooks \
  -H "Authorization: Bearer <YOUR_API_KEY>"
```

```json theme={null}
{
  "webhooks": [
    {
      "id": "9d3c1f70-6a25-4e88-b014-2f7a5c9e3d61",
      "site_id": "ttid_7Rb4TrC1dTbnD8w3s1TS12",
      "url": "https://hooks.example.com/traceten",
      "event_types": ["ai_session.classified"],
      "enabled": true,
      "verification_status": "verified",
      "created_at": "2026-07-04T12:00:00.000Z",
      "updated_at": "2026-08-12T09:31:20.000Z"
    }
  ]
}
```

Signing secrets are never in a listing. They appear only when you create an endpoint or rotate its secret.

`verification_status` is diagnostic:

| Value        | Meaning                                                    |
| ------------ | ---------------------------------------------------------- |
| `verified`   | A ping reached this endpoint and it answered 2xx.          |
| `failing`    | It was verified once and a later ping failed.              |
| `unverified` | No ping has ever succeeded. It may simply not be live yet. |

## `POST /v1/sites/{siteId}/webhooks`

```bash theme={null}
curl -X POST https://api.traceten.com/v1/sites/ttid_7Rb4TrC1dTbnD8w3s1TS12/webhooks \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://hooks.example.com/traceten",
    "event_types": ["ai_session.classified"]
  }'
```

```json theme={null}
{
  "id": "9d3c1f70-6a25-4e88-b014-2f7a5c9e3d61",
  "site_id": "ttid_7Rb4TrC1dTbnD8w3s1TS12",
  "url": "https://hooks.example.com/traceten",
  "event_types": ["ai_session.classified"],
  "enabled": true,
  "verification_status": "verified",
  "created_at": "2026-08-29T12:04:44.000Z",
  "updated_at": "2026-08-29T12:04:44.000Z",
  "secret": "whsec_EXAMPLE"
}
```

<Warning>
  `secret` is returned once, here. Store it before you discard the response. No read endpoint can
  give it back.
</Warning>

`url` must be `https://`, must not embed credentials, and must not point at a private, loopback or internal address. `event_types` defaults to `["ai_session.classified"]`.

Registration sends one real ping through the delivery path, which is why `verification_status` is often already `verified` in this response. A failed ping does not block creation, it just leaves the endpoint `unverified`.

Ten webhooks per site.

## `PATCH /v1/sites/{siteId}/webhooks/{webhookId}`

Send at least one of `url`, `event_types`, `enabled`.

```bash theme={null}
curl -X PATCH https://api.traceten.com/v1/sites/ttid_7Rb4TrC1dTbnD8w3s1TS12/webhooks/9d3c1f70-6a25-4e88-b014-2f7a5c9e3d61 \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{ "enabled": false }'
```

Returns the updated webhook.

## `DELETE /v1/sites/{siteId}/webhooks/{webhookId}`

Removes the endpoint and its delivery log. Returns `204` with no body. Unlike site deletion this is immediate; there is no window and no restore.

## `GET /v1/sites/{siteId}/webhooks/{webhookId}/deliveries`

```bash theme={null}
curl "https://api.traceten.com/v1/sites/ttid_7Rb4TrC1dTbnD8w3s1TS12/webhooks/9d3c1f70-6a25-4e88-b014-2f7a5c9e3d61/deliveries?limit=50&status=failed" \
  -H "Authorization: Bearer <YOUR_API_KEY>"
```

```json theme={null}
{
  "deliveries": [
    {
      "id": "5b2e8c14-9f03-4a67-8d51-1c7b0e4a2f38",
      "webhook_id": "9d3c1f70-6a25-4e88-b014-2f7a5c9e3d61",
      "event_type": "ai_session.classified",
      "envelope_id": "whd_2f7a5c9e-3d61-4e88-b014-9d3c1f706a25",
      "dedupe_key": "sess_8a3f0c15d2e947b6",
      "status": "failed",
      "attempt_count": 5,
      "response_status": 500,
      "last_error": "HTTP 500",
      "next_attempt_at": null,
      "last_attempt_at": "2026-08-29T10:02:41.118Z",
      "created_at": "2026-08-29T09:44:02.000Z",
      "payload": "{\"id\":\"whd_2f7a5c9e...\"}"
    }
  ],
  "next_before": "2026-08-29T09:44:02.000Z"
}
```

Newest first. Pass the previous page's `next_before` back as `before` to page. `next_before` is `null` when the page was not full.

| Parameter | Type    | Notes                                           |
| --------- | ------- | ----------------------------------------------- |
| `limit`   | integer | 1 to 100. Defaults to 50.                       |
| `status`  | string  | `pending`, `retrying`, `succeeded` or `failed`. |
| `before`  | string  | ISO-8601 cursor.                                |

<Note>
  Test pings appear in this log with `event_type: "webhook.ping"`. Exclude them before you compute a
  success rate, or your own test sends will skew it.
</Note>

## `POST /v1/sites/{siteId}/webhooks/{webhookId}/test`

Sends one signed ping through the same path as real events and returns the outcome inline. Allowed on a disabled webhook, so you can prove an endpoint before switching it on.

```json theme={null}
{
  "ok": true,
  "status_code": 200,
  "latency_ms": 142,
  "response_snippet": "OK",
  "verification_status": "verified"
}
```

A ping is terminal after one attempt; it never enters the retry schedule. A `429` means the endpoint is currently rate-capped, nothing was proved either way, and `verification_status` is untouched. Click again in a moment.

<Note>
  The ping **payload** we deliver to your endpoint carries `site_id` as the site's internal UUID,
  not the snippet key you used in the request URL. That is a property of the outbound webhook
  contract rather than this API, and it differs from `ai_session.classified` and
  `conversion.attributed`, which carry the `ttid_`. See [event payloads](/webhooks/events).
</Note>

This needs `config:write`, not `stats:read`. It causes Traceten to make a real outbound request on your behalf, which is not a read.

## `POST /v1/sites/{siteId}/webhooks/{webhookId}/rotate-secret`

```json theme={null}
{
  "id": "9d3c1f70-6a25-4e88-b014-2f7a5c9e3d61",
  "secret": "whsec_EXAMPLE_ROTATED"
}
```

<Warning>
  **Destructive.** The old secret stops verifying immediately. There is no overlap window, so any
  receiver still checking against the old value will reject every delivery until you deploy the new
  one. Deploy a receiver that accepts both, rotate, then remove the old one.
</Warning>

The new secret is returned once, here.

## Errors

| Status | `error`              | Cause                                                                               |
| ------ | -------------------- | ----------------------------------------------------------------------------------- |
| `401`  | `unauthorized`       | Missing, malformed, revoked or expired key.                                         |
| `403`  | `insufficient_scope` | The key lacks the permission this endpoint needs. `required_scope` names it.        |
| `403`  | `Forbidden`          | The site is not one this credential can reach, or it is inside its deletion window. |
| `404`  | none                 | Unknown webhook id on that site.                                                    |
| `422`  | `validation_failed`  | Bad URL or body.                                                                    |
| `429`  | none                 | On `test`, the endpoint is rate-capped.                                             |

## Next

* [Verifying signatures](/webhooks/verify-signatures)
* [Event payloads](/webhooks/events)
* [Delivery and retries](/webhooks/delivery)
