> For the complete documentation index, see [llms.txt](https://docs.nerovasystems.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.nerovasystems.com/guides/build-well/webhooks.md).

# Receive events with webhooks

Build a webhook receiver that verifies signatures, deduplicates events, and survives redelivery. A declared event name or availability flag is not evidence that ordinary application activity emits it; retain polling where delivery has not been established.

> **Available.** Webhook endpoints are created and managed in the console and on the `tenant-v1` machine contract under `/api/v1/tenants/{tenantId}/webhooks` with the `webhook:manage` scope — endpoint CRUD, pause/resume, secret rotation, the event catalog, the delivery ledger with redrive, stats, and test delivery. See [Lifecycle and availability](https://docs.nerovasystems.com/documentation/api/lifecycle). Everything below documents the delivery contract your receiver verifies.

## What you'll build

An HTTPS endpoint that accepts signed event envelopes from Nerova, verifies each one against your endpoint's signing secret, acknowledges fast, and processes idempotently.

## Prerequisites

* A machine API key with the `webhook:manage` scope and a grant for the tenant — or console access with the Owner or Admin role if you prefer to manage endpoints from **Developers → Webhooks**.
* A publicly reachable HTTPS URL. Plain HTTP endpoints are rejected.

## The delivery model

* **At-least-once, no ordering promise.** The same event can arrive more than once, and later events can arrive first. Deduplicate on the envelope `id` and order by `occurred_at`.
* **Per-endpoint secrets and versions.** Every endpoint gets its own signing secret (`whsec_…`, shown once at creation) and pins the payload schema version current at creation (currently `2026-07-01`).
* **Live events only.** Endpoints receive the events your Live key and its tenants produce. The only deliveries marked `"livemode": false` are the synthetic test deliveries you trigger yourself (see [Operate your endpoints](#operate-your-endpoints)).
* **Content-free payloads.** Event snapshots carry reason codes, states, and identifiers — never conversation content or transcripts, consistent with [Security and data ownership](https://docs.nerovasystems.com/documentation/security/data-ownership).

## The event catalog

Endpoints subscribe to exact event types or whole families with a wildcard (`capability.*`). The first four events below are emitted by the platform when the AI acts and are the ones to build usage tracking on; see [Track and limit usage by your plans](/guides/observe-and-account/track-and-limit-usage.md). The other entries are declared names, and the escalation, capability, and tenant entries carry the delivery limitation below:

| Event                      | Meaning or delivery limitation                                                                                       |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `conversation.started`     | An AI conversation started: the first AI-handled message from a customer on a channel in 24 hours, or one voice call |
| `booking.created`          | The AI created a booking on the tenant's platform                                                                    |
| `booking.cancelled`        | The AI cancelled a booking on the tenant's platform                                                                  |
| `usage.threshold_crossed`  | The tenant's monthly token usage crossed the soft cap or reached the hard cap you set; once per cap per UTC month    |
| `capability.suspended`     | A verified capability failed its check; the AI employee stops using it and fails closed                              |
| `capability.restored`      | The failing check passed again; the capability is back on                                                            |
| `escalation.raised`        | Retained compatibility name for human attention; ordinary emission and delivery are unproven                         |
| `escalation.resolved`      | Retained compatibility name for closure; ordinary emission and delivery are unproven                                 |
| `tenant.activated`         | Provisioning finished and validation passed; the tenant is live                                                      |
| `tenant.validation_failed` | Activation stalled on a failed capability check; the snapshot names the failing step                                 |

**Escalation delivery is not established.** The [authored local retirement](https://docs.nerovasystems.com/documentation/console/platform-access#campaign-and-escalation-retirement) removes the explicit routing dispatcher traced to `escalation.raised`. No ordinary emitter for `escalation.resolved` was established in that source trace. Compatibility names and catalog availability flags remain unchanged; no replacement emitter or event pipeline has been introduced.

Poll the [attention API](https://github.com/Nerova-Systems/Project-Songbird/tree/main/docs/guides/attention.md) for persisted open records. Creating, acknowledging, resolving, or handing off an attention item does not establish that one of these events fires. The source changes were still awaiting generation and compilation at handoff and are not a deployment claim.

**Capability and tenant event delivery is not established either.** No ordinary code path in source flips a capability to suspended or restored (a failing coverage check raises a notification, not a capability state change), and no emitter for `tenant.activated` or `tenant.validation_failed` was established. Poll the capability report and the activation manifest instead.

The usage and counting events behave as follows:

* `conversation.started`, `booking.created`, and `booking.cancelled` fire once per occurrence and carry ids you can deduplicate on. Their snapshots are shown in [Track and limit usage by your plans](/guides/observe-and-account/track-and-limit-usage.md#1-count-with-webhooks).
* `usage.threshold_crossed` fires when a tenant's monthly token usage reaches the soft cap or the hard cap you set with `PUT …/usage/limits`, at most once per tenant, per cap, per UTC calendar month. The snapshot carries `threshold` (`soft_cap` or `hard_cap`), `unit` (`tokens`), `month` (`YYYY-MM`), `tokens_used`, `cap`, and `crossed_at`.

The catalog also publishes coming-soon events (`channel.disconnected`, `channel.quality_changed`, `task.completed`, `invoice.issued`, `usage.snapshot`) so you can plan, but they cannot be subscribed to yet. The console's catalog view is the authoritative list of declared subscription availability, not proof of an emitter or a delivered message.

## The envelope

Every delivery is an HTTP `POST` with a JSON envelope. Keys are snake\_case; this shape is the wire contract:

```json
{
  "id": "…",
  "type": "capability.suspended",
  "api_version": "2026-07-01",
  "occurred_at": "2026-07-01T12:00:00Z",
  "tenant": "…",
  "livemode": true,
  "data": {
    "object": { }
  }
}
```

`data.object` is the full snapshot of the affected object at `occurred_at` — you should not need a follow-up read to act on the event. `livemode` is `true` for real events and `false` only for synthetic test-fire deliveries.

## Verify the signature

Each delivery carries a `Nerova-Signature` header:

```
Nerova-Signature: t=1767225600,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
```

`t` is the send time in Unix seconds; `v1` is a lowercase-hex HMAC-SHA256 of the string `"{t}.{raw body}"`, keyed with your endpoint's secret. During the 24-hour grace window after a secret rotation the header carries a second `v1` entry signed with the previous secret — accept the delivery if **any** `v1` matches.

Verification rules:

1. Compute the HMAC over the **raw request bytes** — before any JSON parsing or re-serialization.
2. Compare in constant time.
3. Reject when `|now − t|` exceeds **5 minutes** — this closes replay.

```js
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody, signatureHeader, secret) {
  const parts = signatureHeader.split(",");
  const timestamp = Number(parts.find((p) => p.startsWith("t=")).slice(2));
  if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;

  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");
  return parts
    .filter((p) => p.startsWith("v1="))
    .some((p) => {
      const candidate = Buffer.from(p.slice(3), "hex");
      const wanted = Buffer.from(expected, "hex");
      return candidate.length === wanted.length && timingSafeEqual(candidate, wanted);
    });
}
```

Treat the signing secret like any credential: store it in your secret manager, never in code or logs.

## Respond fast, process later

Return a `2xx` quickly — enqueue the event and do the real work asynchronously. Any non-`2xx` response, timeout, or connection failure counts as a failed attempt.

Retries back off exponentially: after the first failure the delivery retries in 10 minutes, then 20, 40, 80, 160, 320, and 640 minutes — **8 attempts over roughly 21 hours**. After the final failure the delivery dead-letters and **the endpoint is paused automatically** and surfaced as needing attention in the console. Resume it from the console once your receiver is healthy; failed deliveries can be redriven individually while their events are retained (**30 days**).

```mermaid
sequenceDiagram
    participant N as Nerova delivery pipeline
    participant R as Your receiver

    N->>R: POST envelope + Nerova-Signature
    alt acknowledged with 2xx
        R-->>N: 2xx
        Note over N: delivery Succeeded
    else non-2xx, timeout, or connection failure
        R--xN: failed attempt
        Note over N: back off — 10 min, doubling each attempt
        N->>R: attempts 2…8, fresh signature timestamp each time
        Note over N: after attempt 8 the delivery dead-letters<br/>and the endpoint is paused
    end
```

Because redelivery and redrive are normal, your handler must be idempotent: record the envelope `id` you have processed and skip duplicates.

## Operate your endpoints

From the console's webhooks page or the API (`/api/v1/tenants/{tenantId}/webhooks`, mutations require an `Idempotency-Key` header) you can:

* **Create endpoints** with a URL, description, and at least one event filter. The signing secret is displayed once — copy it then.
* **Send a test delivery** — a synthetic sample payload for a chosen event type, signed with the endpoint's real secret, marked `"livemode": false`, and pushed through the same delivery and retry pipeline as real events.
* **Inspect deliveries** — status, attempt history with response codes and durations, the exact payload, and the (truncated) response body your server returned. The API pages the ledger with a cursor: pass `nextCursor` back as `cursor` until it comes back null.
* **Redrive failures** and **pause/resume** endpoints.
* **Rotate secrets.** Rotation issues a new secret (shown once) and keeps the old one signing alongside it for 24 hours, so you can swap receivers without dropping an event.

## Polling remains an option

You do not need webhooks to integrate. The `/api/v1` surface is poll-friendly: the work ledger (`GET …/work`), attention items (`GET …/attention`), the activity feed (`GET …/activity`), and the activation manifest all expose the same state transitions the events announce — the [error-handling patterns](/guides/build-well/error-handling.md) are identical either way.

## Next steps

* Trigger the events you'll consume: [Provision and activate tenants](/guides/provision/partner-playbook.md).
* Check current surface availability: [Lifecycle and availability](https://docs.nerovasystems.com/documentation/api/lifecycle).
* Go-live gates for receivers: [Go-live checklist](/guides/provision/go-live.md).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.nerovasystems.com/guides/build-well/webhooks.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
