# Webhook endpoints

> Choose where Proof sends events, and get the secret to check they came from Proof.

A webhook endpoint is a URL on your server where Proof sends events: a comment, a mention, a suggestion accepted, a document an agent created, a seal. You turn them into notifications, emails and updates in your own lists. You can manage endpoints here or in the [console](https://developer.proof.exalto.ai/console). See [Send notifications](/guides/notifications/) for a worked example and [Webhook events](/reference/webhooks/) for every event and its payload.

Test and live mode have separate endpoints: events from test documents go only to endpoints created with a test key.

## The webhook endpoint object

| Attribute | Type | Description |
| --- | --- | --- |
| `id` | string | The endpoint's id, starting `we_`. |
| `url` | string | Where Proof posts events. |
| `events` | array | The event types it receives, or `["*"]` for all of them, including types added later. |
| `created_at` | timestamp | When it was created. |
| `secret` | string | The signing secret, starting `whsec_`. Returned only when you create the endpoint or roll its secret. |

```json
{
  "id": "we_5Kd2Hs8N",
  "url": "https://acme.com/proof/events",
  "events": ["thread.created", "thread.replied", "mention.created", "seal.created"],
  "created_at": "2026-10-30T18:20:00Z"
}
```

## Create a webhook endpoint

`POST /v1/webhook-endpoints`

| Parameter | Type | Description |
| --- | --- | --- |
| `url` | string, required | An `https` URL. With a test key, any URL, such as `http://localhost:3000/proof/events` or a tunnel. |
| `events` | array, required | Event types, such as `["mention.created", "seal.created"]`, or `["*"]` for all. |

<Tabs syncKey="lang">
<TabItem label="curl">

```bash
curl https://api.proof.exalto.ai/v1/webhook-endpoints \
  -H "Authorization: Bearer $PROOF_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://acme.com/proof/events",
    "events": ["thread.created", "thread.replied", "mention.created", "seal.created"]
  }'
```

</TabItem>
<TabItem label="Node">

```ts
const endpoint = await proof.webhookEndpoints.create({
  url: "https://acme.com/proof/events",
  events: ["thread.created", "thread.replied", "mention.created", "seal.created"],
});
// Store endpoint.secret now: it isn't shown again.
```

</TabItem>
</Tabs>

Returns the [endpoint](#the-webhook-endpoint-object) with its `secret`, with `201`.

```json
{
  "id": "we_5Kd2Hs8N",
  "url": "https://acme.com/proof/events",
  "events": ["thread.created", "thread.replied", "mention.created", "seal.created"],
  "created_at": "2026-10-30T18:20:00Z",
  "secret": "whsec_R4n8Vq2Lk7Tz1Mx9Pw3Hc6Bs"
}
```

| Error | When |
| --- | --- |
| `400 invalid_parameter` on `url` | Not a URL, or, with a live key, not `https`. |
| `400 invalid_parameter` on `events` | An event type doesn't exist. |

## List webhook endpoints

`GET /v1/webhook-endpoints`

Your endpoints in the current mode, newest first. Secrets aren't included.

| Parameter | Type | Description |
| --- | --- | --- |
| `limit` | integer | 1 to 100. Default 50. |
| `after` | string | The `next` cursor from the previous page. |

<Tabs syncKey="lang">
<TabItem label="curl">

```bash
curl https://api.proof.exalto.ai/v1/webhook-endpoints \
  -H "Authorization: Bearer $PROOF_SECRET_KEY"
```

</TabItem>
<TabItem label="Node">

```ts
const page = await proof.webhookEndpoints.list();
```

</TabItem>
</Tabs>

```json
{
  "data": [
    {
      "id": "we_5Kd2Hs8N",
      "url": "https://acme.com/proof/events",
      "events": ["thread.created", "thread.replied", "mention.created", "seal.created"],
      "created_at": "2026-10-30T18:20:00Z"
    }
  ],
  "next": null
}
```

## Roll a signing secret

`POST /v1/webhook-endpoints/{endpoint_id}/roll-secret`

Issues a new signing secret. For the next 24 hours Proof signs each event with both the old and the new secret, so you can deploy the new one without dropping events. See [Verifying signatures](/reference/webhooks/).

| Parameter | Type | Description |
| --- | --- | --- |
| `endpoint_id` | string, path, required | The endpoint's id. |

<Tabs syncKey="lang">
<TabItem label="curl">

```bash
curl https://api.proof.exalto.ai/v1/webhook-endpoints/we_5Kd2Hs8N/roll-secret \
  -X POST \
  -H "Authorization: Bearer $PROOF_SECRET_KEY"
```

</TabItem>
<TabItem label="Node">

```ts
const { secret } = await proof.webhookEndpoints.rollSecret("we_5Kd2Hs8N");
```

</TabItem>
</Tabs>

Returns the [endpoint](#the-webhook-endpoint-object) with its new `secret`.

## Delete a webhook endpoint

`DELETE /v1/webhook-endpoints/{endpoint_id}`

Stops sending events to the endpoint. Events already on their way, including retries, are dropped.

| Parameter | Type | Description |
| --- | --- | --- |
| `endpoint_id` | string, path, required | The endpoint's id. |

<Tabs syncKey="lang">
<TabItem label="curl">

```bash
curl https://api.proof.exalto.ai/v1/webhook-endpoints/we_5Kd2Hs8N \
  -X DELETE \
  -H "Authorization: Bearer $PROOF_SECRET_KEY"
```

</TabItem>
<TabItem label="Node">

```ts
await proof.webhookEndpoints.delete("we_5Kd2Hs8N");
```

</TabItem>
</Tabs>

```json
{
  "id": "we_5Kd2Hs8N",
  "deleted": true
}
```

## Delivery

Each event is a `POST` with a JSON body and a `Proof-Signature` header. Check the signature before you trust the body: `proof.webhooks.verify(rawBody, header, secret)` does it in Node. Answer with any `2xx` within 10 seconds; anything else, or no answer, is retried with backoff for 3 days. Events can arrive more than once or out of order, so skip ids you've already handled. The [webhook events reference](/reference/webhooks/) has the signature format and every event's payload.