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

# Webhooks

> Register an HTTPS endpoint. orriven sends a signed POST when something happens in a Business Unit — event types, signatures, retries, and the delivery log.

A **webhook endpoint** is an HTTPS address. When something happens in a Business Unit — a registration confirmed, an order placed, a person checked in — orriven sends a POST to that address. The receiving server does not need to poll the [Developer API](/en/developers/overview). Each request is signed and recorded.

Endpoints are managed on the **Webhooks** tab of [Developer tools](/en/developers/dev-tools). An endpoint belongs to one **Business Unit** and receives events from **every event in that unit**. The payload includes the event, so filtering happens on the receiving side.

<Info>
  The same operations are available from the [command-line tool](/en/developers/cli#webhooks) and from an agent through the [MCP server](/en/agent/tools#webhooks).
</Info>

## Who can manage endpoints

Organization **Owners and Admins**. An endpoint receives this Business Unit's event stream, including names and email addresses. Registering one is the same class of decision as issuing an [API key](/en/developers/api-keys).

## Registering an endpoint

<Steps>
  <Step title="Open the drawer and choose Webhooks">
    The drawer's Business Unit selector sets which unit the endpoint belongs to.
  </Step>

  <Step title="Add endpoint">
    * **Endpoint URL** — must be `https://`. Plain `http://` is accepted only for `localhost`, for local development.
    * **Description** — optional.
    * **Events** — select the event types this endpoint should receive. At least one is required. An endpoint with nothing selected receives **nothing**, not every event.
  </Step>

  <Step title="Copy the signing secret">
    Choose **Deliveries** on the endpoint. The **signing secret** is shown above the log and stays visible. The receiving server needs a copy to verify signatures. Unlike an API key secret, it can be viewed again.
  </Step>

  <Step title="Send a test">
    **Send test** queues a `webhook.ping` event to this endpoint. It uses the same signature, retry, and log path as a real event.
  </Step>
</Steps>

## Event types

The list of subscribable events matches the triggers used by [automations](/en/marketing/automations#triggers). A new trigger becomes a subscribable event when it is added.

| Event                     | Fires when                                                                          |
| ------------------------- | ----------------------------------------------------------------------------------- |
| `attendee.added`          | An organizer registered someone from the console (any status).                      |
| `registration.confirmed`  | A registration became confirmed — checkout completed, or the organizer approved it. |
| `registration.pending`    | A registration on an approval ticket type is awaiting review.                       |
| `registration.waitlisted` | The ticket type was full and takes a waitlist.                                      |
| `registration.rejected`   | The organizer rejected a pending registration.                                      |
| `registration.checked_in` | Event-level (front desk) check-in. Session and venue door scans do not fire it.     |
| `order.placed`            | A buyer placed an order; it is holding its seats.                                   |
| `order.confirmed`         | The order settled and produced its registration.                                    |
| `order.cancelled`         | The buyer cancelled, or the console released the hold.                              |
| `order.expired`           | A pending order's hold lapsed unconfirmed.                                          |
| `exhibitor.confirmed`     | An exhibitor was confirmed.                                                         |
| `booth.assigned`          | A booth unit was assigned to an exhibitor.                                          |
| `stay.confirmed`          | A stay was confirmed by the hotel.                                                  |
| `stay.checked_in`         | The guest checked in at the hotel.                                                  |
| `survey.submitted`        | A survey response — first submission only.                                          |
| `lead.captured`           | An exhibitor captured a visitor's badge as a lead — first capture only.             |

The one automation trigger that is **not** a webhook event is *Before the event starts*: that is an automation timer, not a change to a person or an order.

## Request format

One `POST` per event, `Content-Type: application/json`, user agent `orriven-webhooks/1.0`, with these headers:

| Header                | Value                                                                                                                                 |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `orriven-signature`   | `t=<unix seconds>,v1=<hex HMAC-SHA256>` — see below.                                                                                  |
| `orriven-event-id`    | The event's id (`evt_…`). The same event sent to several endpoints, or resent, carries the **same** id.                               |
| `orriven-event-type`  | The event type, e.g. `registration.confirmed`.                                                                                        |
| `orriven-delivery-id` | This delivery attempt's id.                                                                                                           |
| `orriven-version`     | The [API version](/en/developers/versioning) that shapes the payload — always the platform's current version, not a key's pinned one. |

The body is JSON with `id`, `type`, `createdAt`, `apiVersion`, and `data`. Inside `data`, the `event` block is always present. `registration`, `order`, `exhibitor`, and `contact` are present when the event is about one of them and `null` otherwise — `order.expired` has no registration yet; `survey.submitted` has no order. Read `type` to know which blocks to expect.

```json theme={null}
{
  "id": "evt_8f2c…",
  "type": "registration.confirmed",
  "createdAt": "2026-08-28T09:15:02.000Z",
  "apiVersion": "2026-08-22",
  "data": {
    "event": {
      "id": "…",
      "publicId": "…",
      "name": "Summit 2026",
      "status": "published",
      "currency": "USD"
    },
    "registration": {
      "id": "…",
      "status": "confirmed",
      "type": "general",
      "registrationTypeId": "…",
      "redemptionCodeId": null,
      "promotionLinkId": null,
      "checkinCode": "CHK-…",
      "passUrl": "https://pages.orriven.com/pass/reg_…",
      "registeredAt": "2026-08-28T09:15:01.000Z"
    },
    "order": {
      "id": "…",
      "orderNumber": "…",
      "status": "paid",
      "totalAmount": 12000,
      "discountAmount": 0,
      "currency": "USD",
      "promotionLinkId": null,
      "paidAt": "2026-08-28T09:15:01.000Z"
    },
    "exhibitor": null,
    "contact": { "email": "ada@example.com", "name": "Ada Lovelace" }
  }
}
```

* Amounts are integers in the currency's **minor units** (`12000` is 120.00 USD).
* `registration.passUrl` is the person's [hosted entry pass](/en/events/hosted-pages). A receiver of `registration.confirmed` has the link without a second call.
* A `webhook.ping` test carries `data: { "endpointId": "…" }` and nothing else.

## Verifying the signature

Every delivery is signed with the endpoint's secret in the Stripe scheme: the signed material is the timestamp, a dot, and the **raw request body** — `t.body` — and `v1` is its hex HMAC-SHA256. Because the timestamp is inside the signed material, a captured delivery cannot be re-dated. Reject anything older than a few minutes to cover replays.

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

const TOLERANCE_SECONDS = 300;

export function verifyOrrivenSignature(secret, rawBody, header) {
  const parts = Object.fromEntries(
    header.split(",").map((part) => part.trim().split("=", 2)),
  );
  const timestamp = Number(parts.t);
  if (!parts.v1 || !Number.isFinite(timestamp)) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > TOLERANCE_SECONDS) {
    return false;
  }

  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");
  const given = Buffer.from(parts.v1, "hex");
  const want = Buffer.from(expected, "hex");
  return given.length === want.length && timingSafeEqual(given, want);
}
```

Verify against the **exact bytes** received — in Express, read the body with `express.raw({ type: "application/json" })` and parse it only after the check. A body that has been parsed and re-serialized will not match.

## Delivery and retries

* A response with any **2xx** status within **10 seconds** counts as delivered. Redirects are not followed and do not count. Re-register an endpoint that has moved.
* Anything else — a non-2xx status, a timeout, a DNS or TLS failure — is retried on a fixed schedule: immediately, then **1 minute, 5 minutes, 30 minutes, 2 hours, and 6 hours** after the previous attempt. That is **6 attempts over roughly eight and a half hours**. After the last attempt the delivery is marked **Failed** and only a manual resend follows.
* Delivery is **at least once**, not exactly once. Retries and resends carry the same `orriven-event-id`; deduplicate on it.
* **Order is not guaranteed.** A retried event can arrive after a newer one. Sort on `createdAt` if sequence matters.
* A failing endpoint never blocks or rolls back the registration or order that produced the event.

## The delivery log

The endpoint list shows each endpoint's URL, how many events it subscribes to, its **Health** — *Delivering*, *N failures in a row*, or *Nothing sent yet* — and whether it is **Enabled** or **Disabled**. **Deliveries** opens the log for one endpoint, newest first:

| Column   | Meaning                                                                                                                                |
| -------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| Queued   | When the delivery was created.                                                                                                         |
| Event    | The event type.                                                                                                                        |
| Status   | **Queued**, **Sending**, **Delivered**, or **Failed** (the retry schedule ran out).                                                    |
| Attempts | How many times it has been tried so far.                                                                                               |
| Response | The HTTP status the server returned, or the error when no response arrived. The first 2 KB of the response body is kept for diagnosis. |

**Resend** on any row creates a **new** delivery carrying the original event id. The log still records that the first attempt failed. The receiving server can treat it as the same event. **Edit events** changes the subscription; only the events selected reach the endpoint from then on.

## Disabling an endpoint

**Disable** stops deliveries at once. Anything still queued for that endpoint ends as *Failed* with the reason "Endpoint is disabled." **Enable** turns it back on. An endpoint is **never deleted** — the row is the record that this integration existed, and its log stays readable.

## Webhooks in automations

The **call a webhook** action on the [automation canvas](/en/marketing/automations) can point at a registered endpoint instead of a bare URL. A registered endpoint gets the signature, the retry schedule, and the delivery log. A bare URL remains a one-shot call.

## Rules

* **Endpoints are Business Unit-wide.** There is no per-event subscription. Filter on `data.event` on the receiving side.
* **An empty subscription means nothing**, not "all events".
* **The secret is shared and can be viewed again.** It signs every delivery to that endpoint. If it leaks, register a new endpoint and disable the old one.
* **Disabled, never deleted.** Endpoint changes — creation, subscription edits, disabling, resends — are recorded in the [audit log](/en/organization/audit-logs).
* **The platform does not disable an endpoint.** Repeated failures update the Health column. Stopping the integration is a console action.

## Related

<CardGroup cols={2}>
  <Card title="Developer tools" icon="terminal" href="/en/developers/dev-tools">
    The drawer that contains the Webhooks tab.
  </Card>

  <Card title="Command-line tool" icon="square-terminal" href="/en/developers/cli">
    Register, ping, and resend from the terminal.
  </Card>

  <Card title="Automations" icon="diagram-project" href="/en/marketing/automations">
    The same triggers, acted on inside the platform.
  </Card>

  <Card title="Agents" icon="robot" href="/en/agent/tools">
    Endpoint and delivery tools on the MCP server.
  </Card>
</CardGroup>
