# Webhooks
Source: https://nordvec.com/docs/guides/webhooks#event-catalog

## Event catalog [#event-catalog]

Subscribe an endpoint to any combination of the event types below. Types
marked **planned** can already be selected, but nothing fires for them yet and
their payload shape is not final; it is published here when the event goes
live.

| Event | Status | Payload (`data`) |
| ----- | ------ | ---------------- |
| `documents.indexed` | Live | Documents became searchable. Aggregated per workspace: at most one event per aggregation window, carrying a count. |
| `documents.failed` | Live | Documents terminally failed processing. Aggregated per workspace: at most one event per aggregation window, carrying a count. |
| `sync.completed` | Planned | A connector sync run completed. Payload shape is not final. |
| `sync.failed` | Planned | A connector sync run failed. Payload shape is not final. |
| `ingestion.completed` | Planned | An ingestion run completed. Payload shape is not final. |
| `ingestion.failed` | Planned | An ingestion run failed. Payload shape is not final. |
| `audit.recorded` | Live | The workspace's audit trail gained entries. Aggregated per workspace over a window: a count and the window to pull, never the entries themselves. |
| `endpoint.disabled` | Live | A webhook endpoint was disabled: automatically after sustained delivery failures or a 410 Gone, or by an admin. Delivered to the workspace's other endpoints. |
| `delivery.failing` | Live | Deliveries to a webhook endpoint are failing: raised once per failure window, 30 minutes after its first failed attempt, and delivered to the workspace's other endpoints. |
| `endpoint.enabled` | Live | A disabled webhook endpoint was switched back on. Closes the incident `endpoint.disabled` opened; delivered to the workspace's other endpoints. |
| `delivery.recovered` | Live | A delivery succeeded on an endpoint that `delivery.failing` was raised for. Closes that incident; delivered to the workspace's other endpoints. |
| `webhook.ping` | Live | Directed test event sent by the dashboard's send-test action to exactly one endpoint. Never subscribable; `data` is always empty. |

Example `data` objects for the live events:

```json
// documents.indexed
{ "count": 12 }

// documents.failed
{ "count": 2 }

// audit.recorded
{
  "count": 34,
  "windowStart": "2026-07-23T09:05:00.000Z",
  "windowEnd": "2026-07-23T09:10:00.000Z"
}

// endpoint.disabled
{
  "endpointId": "0198f2a4-6c1e-7d30-b6a1-2f9d54c08a11",
  "reason": "sustained_failure",
  "disabledAt": "2026-07-23T09:14:07.000Z"
}

// delivery.failing
{
  "endpointId": "0198f2a4-6c1e-7d30-b6a1-2f9d54c08a11",
  "failingSince": "2026-07-23T09:14:07.000Z"
}

// endpoint.enabled
{
  "endpointId": "0198f2a4-6c1e-7d30-b6a1-2f9d54c08a11",
  "enabledAt": "2026-07-24T08:02:51.000Z"
}

// delivery.recovered
{
  "endpointId": "0198f2a4-6c1e-7d30-b6a1-2f9d54c08a11",
  "failingSince": "2026-07-23T09:14:07.000Z",
  "recoveredAt": "2026-07-23T11:40:12.000Z"
}

// webhook.ping
{}
```

The operational events (`delivery.failing`, `delivery.recovered`,
`endpoint.disabled`, `endpoint.enabled`) report on the
health of **other** endpoints: the endpoint an event is about is always
excluded from that event's delivery, since delivering there would be
guaranteed noise.

Every live event is also described in the OpenAPI document at
`/api/openapi.json`, under `webhooks`: the full JSON Schema of the body and the
three signature headers, so you can generate receiver types instead of copying
the examples above. The schema is enforced on our side too: a payload that does
not match it is never sent.

### Feeding an audit trail to a SIEM [#feeding-an-audit-trail-to-a-siem]

`audit.recorded` is the push half of a SIEM feed, and it is deliberately not
the entries: an audit entry names a person, their address and what they
touched, and a webhook payload is stored in the delivery log and sent to a URL
you control, which may resolve anywhere. The event tells you a window closed
with entries in it; you pull the entries over the authenticated API with a key
carrying the `audit:read` scope.

On each event, search the trail with `windowStart` as `startDate` and
`windowEnd` as `endDate`, paging on the cursor until it comes back null. The
search's start bound is inclusive and the window's is not, so a page may
repeat the entry at exactly that instant; deduplicate on entry id, which you
need in any case because delivery is at-least-once.

The catalog endpoint lists every action the trail can record, with the
category each belongs to and a version that changes when the vocabulary does.
Read it once, compare the version on later reads, and treat an action missing
from it as unknown rather than invalid: the trail is append-only and keeps the
spelling each entry was written with.

Windows are contiguous for as long as you stay subscribed: each one starts
where the previous one ended, so a pass that is delayed makes the next window
wider instead of losing what happened in between. A window with no entries
sends nothing.

Reading the trail does not trigger an event. Every search and export is
recorded in the trail as `audit_log_viewed` or `audit_log_exported`, and those
entries are not counted toward `audit.recorded`, so the pull you make in
answer to an event never causes the next one. They stay in the trail: a
search returns them with everything else once some other entry opens a
window.