# Webhooks

Source: https://capacms.com/docs/webhooks

Have Capa call your server when content changes, signed so you can prove the request came from Capa.

A webhook endpoint is a URL on your server that Capa calls when something changes in your project: an entry is published, a model changes, a file is uploaded, a scheduled publish fails. Your site can rebuild a page, purge a cache or update a search index the moment it happens.

## Add an endpoint

1. Open **Developers > Webhooks** in the Capa admin and press **Add endpoint**.
2. Enter the URL. It must be `https`.
3. Pick the events. A new endpoint starts with `instance.published`, `instance.unpublished` and `instance.deleted` ticked.
4. Save, and copy the signing secret (`whsec_…`). It is shown in full once. Store it on your server as `CAPA_WEBHOOK_SECRET`.
5. Choose **Send test event**. A `webhook.test` event goes to this endpoint and to no other.

An endpoint hears about events that happen after it was created. Adding one never replays history.

## What arrives

One event is one `POST` with a JSON body. The key order is fixed, because the exact bytes are what is signed:

```json
{
  "id": "evt_00000000-0000-4000-8000-00000000abcd",
  "type": "instance.published",
  "apiVersion": "2026-09-22",
  "occurredAt": "2026-09-23T14:02:11.004Z",
  "tenantId": "3f6c1b4e-2a71-4d0e-9c55-8b0a1f2e7d34",
  "actor": { "kind": "user", "id": "6d2e4f8a-0c91-4b11-8a77-5c3e0f9b1d22" },
  "data": {
    "instanceId": "8b0a1f2e-7d34-4c55-9e51-0f9b1d225c3e",
    "modelId": "2c7e5b8f-0a13-4d31-9b6a-a4e7c2108f52",
    "modelNamespace": "blog_post",
    "versionId": "5c3e0f9b-1d22-4a77-8b11-6d2e4f8a0c91",
    "versionNumber": 7,
    "publishedAt": "2026-09-23T14:02:11.004Z",
    "title": "Summer Guide",
    "scheduledActionId": null
  },
  "links": {
    "self": "https://api.capacms.com/v2/agent/model-instances/8b0a1f2e-7d34-4c55-9e51-0f9b1d225c3e",
    "public": "https://cdn.capacms.com/api/entries/blog_post/8b0a1f2e-7d34-4c55-9e51-0f9b1d225c3e"
  }
}
```

| Field        | Read it as                                                                                                                                                    |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`         | the event's id. It is the same on every retry and every redelivery, and it is also the `Idempotency-Key` header. Store it and ignore a repeat                 |
| `type`       | one of the [events](https://capacms.com/docs/webhooks/events)                                                                                                                    |
| `apiVersion` | the envelope's version. It changes only when the envelope's shape does                                                                                        |
| `occurredAt` | when the change happened, not when this attempt was sent                                                                                                      |
| `actor`      | who did it: `kind` is `user`, `api_key`, `schedule` or `system`, with an `id` where there is one                                                              |
| `data`       | ids, the model's namespace, `title`, `versionId`, `versionNumber`, `publishedAt`, and whatever else the type carries                                          |
| `links`      | `public` reads the published entry with your read key. It is sent on `instance.published` only. `self` is the resource on the agent API. Either may be absent |

The namespace key is `modelNamespace` on `instance.published`, `instance.unpublished` and `model.published`, and `namespace` on every other type. Read both.

The body says what changed. It does not carry the content, unless you turn on **Include content data** for the endpoint: then `instance.published` carries the published entry in `data.data`. Leave it off unless your receiver cannot call back, since a body that carries content leaks it into every log on the way.

## Fetch what changed

Read the entry by its id, with your own key:

```bash
curl "https://cdn.capacms.com/api/entries/$NAMESPACE/$INSTANCE_ID" \
  -H "x-api-key: $CAPA_KEY" \
  -H 'Capa-Version: 2026-10-01'
```

Take the namespace from `data.modelNamespace` or `data.namespace`, and the id from `data.instanceId`. With a production key the read returns the published entry. After `instance.unpublished` or `instance.deleted` it answers `404 entry_not_found`. A read in the same moment as a publish can still see the old copy. See [Caching](https://capacms.com/docs/concepts/caching).

On `instance.published`, `links.public` is this same read, so you can follow it. `links.self` is the entry on the agent API, `/v2/agent`, which a legacy key reads. A `cap_` key reads it once agent keys are switched on for Capa.

## Next
- [Events](https://capacms.com/docs/webhooks/events): Every event, and a sample of the data it carries.
- [Verify a delivery](https://capacms.com/docs/webhooks/verify): Check the signature, with the SDK or by hand.
- [Retries and redelivery](https://capacms.com/docs/webhooks/delivery): What happens when your server is down.
