> ## 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.

# Webhook

> 登记 HTTPS 端点。业务单元内发生事件时，orriven 发送带签名的 POST：事件类型、签名校验、重试规则与投递记录。

**Webhook 端点**是一个 HTTPS 地址。业务单元内发生事件时——报名确认、订单下单、有人在门口签到——orriven 向该地址发送 POST。接收方无需轮询[开放 API](/zh/developers/overview)。每次请求都带签名并记入记录。

端点在[开发工具](/zh/developers/dev-tools)的 **Webhooks** 标签页中管理。一个端点属于一个**业务单元**，接收该单元下**全部活动**的事件。载荷中带有活动信息，筛选在接收侧完成。

<Info>
  同样的操作也可以在终端里用[命令行工具](/zh/developers/cli#webhook)完成，或由 Agent 通过 [MCP 服务](/zh/agent/tools#webhook)完成。
</Info>

## 谁能管理端点

组织的**所有者（Owner）和管理员（Admin）**。端点会收到本业务单元的事件流，包括姓名与邮箱。登记端点与签发 [API 密钥](/zh/developers/api-keys)属于同一类权限。

## 登记端点

<Steps>
  <Step title="打开抽屉，选择「Webhooks」">
    抽屉标题旁的业务单元选择器决定端点归属于哪个单元。
  </Step>

  <Step title="新增端点">
    * **端点地址**——必须为 `https://`。仅 `localhost` 可使用 `http://`，用于本地联调。
    * **备注**——可选。
    * **事件**——勾选该端点需要接收的事件类型。至少勾选一项。未勾选任何事件的端点**什么都不会收到**，不会视为「全部」。
  </Step>

  <Step title="复制签名密钥">
    在端点上选择**投递记录**。**签名密钥**显示在记录上方，并且始终可见。接收方必须持有一份副本才能验签。与 API 密钥的 Secret 不同，它可以再次查看。
  </Step>

  <Step title="发送测试">
    **发送测试**会向该端点排入一个 `webhook.ping` 事件。它与真实事件使用同一套签名、重试与记录。
  </Step>
</Steps>

## 事件类型

可订阅的事件与[自动化](/zh/marketing/automations)使用的触发器完全相同。新增一个触发器后即可订阅。

| 事件                        | 触发时机                      |
| ------------------------- | ------------------------- |
| `attendee.added`          | 组织者在控制台录入了一位参会者（任意状态）。    |
| `registration.confirmed`  | 报名进入已确认状态——结账完成，或组织者审核通过。 |
| `registration.pending`    | 审核制票种上的报名等待审核。            |
| `registration.waitlisted` | 票种已满且开启候补，报名进入候补。         |
| `registration.rejected`   | 组织者拒绝了一条待审核报名。            |
| `registration.checked_in` | 活动级（前台）签到。场次与场地的门禁扫码不触发。  |
| `order.placed`            | 购票人下单，订单正在占位。             |
| `order.confirmed`         | 订单已结算并生成报名。               |
| `order.cancelled`         | 购票人取消，或控制台释放了占位。          |
| `order.expired`           | 待处理订单的占位到期仍未确认。           |
| `exhibitor.confirmed`     | 展商已确认。                    |
| `booth.assigned`          | 展位单元分配给了展商。               |
| `stay.confirmed`          | 酒店确认了住宿。                  |
| `stay.checked_in`         | 客人在酒店办理了入住。               |
| `survey.submitted`        | 问卷收到回答——仅首次提交。            |
| `lead.captured`           | 展商将访客胸卡采集为线索——仅首次采集。      |

唯一**不是** Webhook 事件的自动化触发器是「活动开始前」：那是自动化自身的定时器，不是某个人或某张订单上的变化。

## 请求格式

每个事件一次 `POST`，`Content-Type: application/json`，用户代理为 `orriven-webhooks/1.0`，并携带以下请求头：

| 请求头                   | 取值                                                                 |
| --------------------- | ------------------------------------------------------------------ |
| `orriven-signature`   | `t=<Unix 秒级时间戳>,v1=<十六进制 HMAC-SHA256>`——见下文。                       |
| `orriven-event-id`    | 事件 id（`evt_…`）。同一事件发往多个端点，或被重新投递时，携带**相同**的 id。                    |
| `orriven-event-type`  | 事件类型，如 `registration.confirmed`。                                   |
| `orriven-delivery-id` | 本次投递的 id。                                                          |
| `orriven-version`     | 载荷所依据的 [API 版本](/zh/developers/versioning)——始终为平台当前版本，而非某把密钥绑定的版本。 |

请求体为 JSON，含 `id`、`type`、`createdAt`、`apiVersion` 与 `data`。`data` 中的 `event` 块始终存在。`registration`、`order`、`exhibitor` 与 `contact` 在事件涉及它们时出现，否则为 `null`——`order.expired` 尚无报名，`survey.submitted` 没有订单。根据 `type` 判断应读取哪些块。

```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" }
  }
}
```

* 金额为该币种**最小单位**的整数（`12000` 即 120.00 USD）。
* `registration.passUrl` 是此人的[托管入场凭证](/zh/events/hosted-pages)。收到 `registration.confirmed` 即拥有链接，无需再次调用。
* `webhook.ping` 测试事件只携带 `data: { "endpointId": "…" }`。

## 验证签名

每次投递都以端点的密钥按 Stripe 的方案签名：签名内容是时间戳、一个点号与**原始请求体**——即 `t.body`——`v1` 是它的十六进制 HMAC-SHA256。时间戳位于签名内容之内，截获的投递无法被改写日期。拒绝超过几分钟的时间戳即可防止重放。

```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);
}
```

对收到的**原始字节**验签——在 Express 中用 `express.raw({ type: "application/json" })` 读取请求体，验签之后再解析。经解析后重新序列化的请求体无法通过校验。

## 投递与重试

* **10 秒**内收到任意 **2xx** 响应即视为送达。重定向不会被跟随，也不算成功。端点迁移后应重新登记。
* 其余情况——非 2xx 状态、超时、DNS 或 TLS 失败——按固定时间表重试：立即一次，之后距上一次分别为 **1 分钟、5 分钟、30 分钟、2 小时、6 小时**。合计 **6 次尝试，跨度约八个半小时**。最后一次仍失败即标记为**已失败**，此后只能手动重新投递。
* 投递保证**至少一次**，不保证恰好一次。重试与重新投递携带同一个 `orriven-event-id`，请以它去重。
* **不保证顺序。** 重试的事件可能晚于更新的事件到达。如需顺序，按 `createdAt` 排序。
* 端点失败不会阻塞或回滚产生该事件的报名或订单。

## 投递记录

端点列表显示每个端点的地址、已订阅事件数、**状况**——「投递正常」「连续失败 N 次」或「尚无投递」——以及**已启用**或**已停用**。**投递记录**打开单个端点的记录，最新在前：

| 列    | 含义                                          |
| ---- | ------------------------------------------- |
| 入队时间 | 投递创建的时间。                                    |
| 事件   | 事件类型。                                       |
| 状态   | **排队中**、**投递中**、**已送达**或**已失败**（重试时间表已用尽）。  |
| 尝试次数 | 至今已尝试的次数。                                   |
| 响应   | 服务器返回的 HTTP 状态码；无响应时显示错误原因。响应体保留前 2 KB 供排查。 |

在任意一行选择**重新投递**，会创建一条携带原事件 id 的**新**投递。记录仍然保留首次失败的事实。接收方可以识别为同一事件。**编辑订阅**修改订阅范围；此后只有勾选的事件会送达该端点。

## 停用端点

**停用**立即停止投递。该端点尚在排队的投递将以「Endpoint is disabled.」为由结束为「已失败」。**启用**可重新开启。端点**不删除**——这一行是此集成存在过的记录，其投递记录始终可查。

## 自动化中的 Webhook

[自动化画布](/zh/marketing/automations)上的**调用 Webhook** 动作可以指向一个已登记的端点，而不是裸地址。已登记端点会获得签名、重试时间表与投递记录。填写裸地址则仍是一次性调用。

## 注意事项

* **端点作用于整个业务单元。** 没有按活动订阅。请在接收侧按 `data.event` 筛选。
* **空订阅意味着什么都不发**，不是「全部事件」。
* **密钥可共享且可再次查看。** 它为发往该端点的每次投递签名。一旦泄露，登记新端点并停用旧端点。
* **只停用，不删除。** 端点的变更——创建、编辑订阅、停用、重新投递——都记入[审计日志](/zh/organization/audit-logs)。
* **平台不会停用端点。** 连续失败只会更新「状况」列。停止集成是控制台操作。

## 相关页面

<CardGroup cols={2}>
  <Card title="开发工具" icon="terminal" href="/zh/developers/dev-tools">
    「Webhooks」标签页所在的抽屉。
  </Card>

  <Card title="命令行工具" icon="square-terminal" href="/zh/developers/cli">
    在终端里登记、测试与重新投递。
  </Card>

  <Card title="自动化" icon="diagram-project" href="/zh/marketing/automations">
    同一套触发器，在平台内部执行。
  </Card>

  <Card title="Agent" icon="robot" href="/zh/agent/tools">
    MCP 服务上的端点与投递工具。
  </Card>
</CardGroup>
