跳到主要内容
浏览文档

维护方

接收项目 Webhook

项目 Webhook 会把 Connector for SaaS 的事件发送到你后端的 HTTPS 端点。你可以用它得知终端用户何时完成或未能完成账号连接,也可以用它接收 Trigger 订阅产生的事件。

一个项目最多可以创建 3 个 Webhook。每个 Webhook 分别选择事件类型和接收的账号授权类型,每次投递都使用该 Webhook 的签名 Secret 签名。默认情况下,Webhook 会收到项目内所有连接账号的事件;如果要把某个账号的事件发到单独的接收端,可以把该账号路由到专用 Webhook。

事件类型

事件类型发送时机
connection.succeeded终端用户通过授权链接或提交凭据完成账号连接。
connection.failed账号连接失败,例如用户取消授权,或 provider 拒绝凭据。
trigger.messageTrigger 订阅产生了事件。见用 Trigger 接收第三方事件。

authTypes 限定 Webhook 只接收对应授权类型账号的事件,可选 oauth2、api_key、custom_credential。连接事件和 Trigger 事件都按它过滤。例如 authTypes: ["oauth2"] 的 Webhook 不会收到 API Key 账号的任何事件。

创建 Webhook

在 OOMOL Console 打开项目,选择项目后进入 Webhooks,点击创建 Webhook。填写 HTTPS 地址,选择事件类型和授权类型后创建。

后端也可以使用 Project API key 创建:

POST https://connector.oomol.com/v1/saas/projects/{projectId}/webhooks
Authorization: Bearer <project-api-key>
Content-Type: application/json
{
  "url": "https://api.example.com/oomol/webhooks",
  "eventTypes": ["connection.succeeded", "connection.failed", "trigger.message"],
  "authTypes": ["oauth2", "api_key", "custom_credential"],
  "description": "Production receiver"
}
字段说明
url必填:公网可访问的 HTTPS 地址。
eventTypes必填:上表中的一个或多个事件类型。
authTypes必填:要接收事件的一个或多个账号授权类型。
scope可选:project(默认)接收所有未路由账号的事件;routed 只接收路由到它的账号的事件。创建后不能修改。
eventVersion可选:该 Webhook 固定使用的消息版本,当前为 1。
description可选:不超过 200 个字符的备注。

响应中的 signingSecret 形如 whsec_…,只返回一次。关闭 Console 对话框或丢弃响应前,先把它保存到后端的密钥管理服务中;之后只能看到 signingSecretPrefix。

使用 PATCH /v1/saas/projects/{projectId}/webhooks/{webhookId} 可以修改地址、事件类型、授权类型、备注或 status(active / disabled)。停用的 Webhook 不会收到投递。scope 不能修改,需要时请另建一个 Webhook。

把单个账号的事件路由到专用 Webhook

当某个连接账号的事件需要发到单独的接收端时,可以路由这个账号。例如把测试账号的事件发到预发环境,或发到某个客户自己运行的接收端。

  1. 创建 "scope": "routed" 的 Webhook。在 Console 中,把接收范围设为仅路由的连接。

  2. 终端用户连接账号时,把该 Webhook 的 ID 作为 webhookId 传入。授权链接(POST /v1/saas/connected-accounts/link)和凭据提交(例如 POST /v1/saas/connected-accounts/api-key)都支持这个字段:

    {
      "providerConfigId": "<provider-config-id>",
      "userId": "customer-1",
      "webhookId": "<routed-webhook-id>",
      "returnUri": "https://app.example.com/connections/done"
    }
  3. 要路由已经连接的账号,或取消路由,调用 PATCH /v1/saas/projects/{projectId}/connected-accounts/{connectedAccountId},请求体为 {"webhookId": "<routed-webhook-id>"} 或 {"webhookId": null}。在 Console 中,打开连接账号,编辑该连接,然后设置事件投递。

账号事件发往
未路由所有 eventTypes 和 authTypes 匹配的 active project Webhook。路由 Webhook 收不到它的事件。
已路由只发往它的路由 Webhook,且该 Webhook 必须为 active,eventTypes 和 authTypes 也要匹配;否则事件不会投递,也不会改发到 project Webhook。
  • webhookId 必须是同一项目中的 routed Webhook,否则返回 404 webhook_not_found。
  • 带 webhookId 发起的连接,其 connection.succeeded 或 connection.failed 事件会发往该 Webhook。
  • 重新连接时不传 webhookId,会保留当前路由。
  • 已路由账号上的 Trigger 订阅会把 trigger.message 事件发往它的路由 Webhook。
  • 删除路由 Webhook 后,路由到它的账号会解除路由,事件重新发往 project Webhook。
  • 路由在事件记录时确定。修改路由后,已排队或正在重试的投递仍发往原来的 Webhook;如果该 Webhook 已停用或删除,这些投递会停止,不会改发到新的 Webhook。
  • 连接账号的响应包含 webhookId,未路由的账号为 null。

校验每次投递

每次投递都是一个带 JSON 请求体的 HTTPS POST,请求头如下:

请求头值
x-oo-signaturet=<Unix 秒>,v1=<十六进制签名>
x-oo-event事件类型,例如 trigger.message。
x-oo-event-idenvelope 的 id,同一事件重试时保持不变。
x-oo-webhook-id接收本次投递的 Webhook。
x-oo-delivery-id投递 ID,与投递记录中的一致。

签名使用签名 Secret 对 <t>.<原始请求体> 计算 HMAC-SHA256,并以十六进制编码。请在解析 JSON 之前,用原始请求体校验:

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

const toleranceSeconds = 300;

export function verifyOomolWebhook(secret: string, rawBody: string, header: string | null) {
  if (!header) return false;
  let timestamp = Number.NaN;
  const signatures: string[] = [];
  for (const part of header.split(",")) {
    const [key, value] = part.split("=", 2);
    if (key === "t") timestamp = Number(value);
    if (key === "v1" && value) signatures.push(value);
  }
  const now = Math.floor(Date.now() / 1000);
  if (!Number.isFinite(timestamp) || Math.abs(now - timestamp) > toleranceSeconds) return false;

  const expected = createHmac("sha256", secret).update(`${timestamp}.${rawBody}`, "utf8").digest();
  return signatures.some((signature) => {
    const actual = Buffer.from(signature, "hex");
    return actual.length === expected.length && timingSafeEqual(actual, expected);
  });
}

校验失败时拒绝请求。只要任意一个 v1 匹配就接受:轮换 Secret 期间,请求头会为每个有效 Secret 各带一个签名。

读取事件 envelope

所有事件使用同一种 envelope。用 id 去重:同一事件在重试和不同 Webhook 之间都保持相同的 id。

{
  "id": "evt_0199d1f4-7b2a-7c3e-9a51-3f0f2b8c1d20",
  "type": "connection.succeeded",
  "version": 1,
  "createdAt": 1800000000000,
  "data": {
    "connectionRequestId": "0199d1f3-1c4e-7a90-b2d3-5e6f7a8b9c0d",
    "externalUserId": "customer-1",
    "account": {
      "id": "0199d1f4-2e5f-7b61-8c72-9d0e1f2a3b4c",
      "alias": "work",
      "status": "active",
      "authType": "oauth2"
    },
    "app": {
      "service": "gmail",
      "providerConfigId": "0199d1a0-5f6e-7d8c-9b0a-1c2d3e4f5a6b",
      "slug": "gmail",
      "displayName": "Gmail"
    }
  }
}

createdAt 是毫秒级 Unix 时间戳。externalUserId 是你的后端传给 Connector 的 userId。connection.failed 事件中,account.id 和 account.status 为 null,data.error 包含 code 和 message。没有经过授权请求的连接(例如直接提交凭据)中,connectionRequestId 为 null。

信任 connection.succeeded 事件前,先确认它对应你的后端发起的某次连接,再保存账号绑定关系。授权请求用 externalUserId 和 connectionRequestId 对照你的后端创建的请求。直接提交凭据时 connectionRequestId 为 null,改用 externalUserId 和 account.id 对照提交接口返回的账号;这个调用返回时,你的后端已经拿到了该账号。trigger.message 的内容见 Trigger 指南中的接收事件。

响应与重试

校验并保存事件后返回 2xx。耗时的处理放到后台任务中执行:投递请求 10 秒超时,并且不跟随重定向。

端点结果Connector 的处理
2xx标记为投递成功。
408、425、429、5xx、超时或网络错误重试投递。
其他 4xx,或 Webhook 已停用、已删除停止向该 Webhook 投递此事件。

重试依次等待 5 秒、30 秒、2 分钟、10 分钟、30 分钟、1 小时和 2 小时,最多 8 次尝试,合计约 3.7 小时。除第一档外,每档等待时间有最多 20% 的随机浮动。如果端点在整个重试期间都不可用,投递会以 failed 结束,该事件不会再发送,可以在投递记录中找到它。如果你的端点已保存事件但确认结果丢失,同一 id 的事件可能再次到达。

测试、查看与轮换

操作ConsoleAPI
发送一条示例 connection.succeeded 事件发送测试POST /v1/saas/projects/{projectId}/webhooks/{webhookId}/test
查看最近的投递尝试投递记录GET /v1/saas/projects/{projectId}/webhooks/{webhookId}/deliveries
更换签名 Secret轮换 SecretPOST /v1/saas/projects/{projectId}/webhooks/{webhookId}/rotate-secret

投递记录保留最近 3 天,每次尝试会显示状态(delivered、retrying 或 failed)、尝试次数、HTTP 状态码和错误信息。

轮换会一次性返回新的 signingSecret。旧 Secret 默认在 24 小时内继续参与签名,请在这段时间内把新 Secret 部署到接收端。可以用 previousSecretExpiresInSeconds 设置其他宽限期,设为 0 时立即停用旧 Secret。再次轮换会结束之前的宽限期。

故障排查

现象检查项
签名始终不匹配在解析或重新序列化 JSON 之前,用原始请求体校验;确认接收端使用的是该 Webhook 的 Secret。
轮换后签名校验失败在旧 Secret 宽限期结束前部署新 Secret。previousSigningSecretExpiresAt 显示宽限期结束时间。
同一事件收到两次验签后按 envelope 的 id 去重。签名不覆盖 x-oo-event-id,不要只凭这个请求头去重。
部分账号收不到事件确认 authTypes 包含该账号的授权类型,且 Webhook 处于 active 状态。如果账号有 webhookId,改为检查该路由 Webhook;project Webhook 不会收到它的事件。
投递一次后即显示 failed端点返回了不可重试的 4xx。检查接收端的鉴权中间件和路由。

让 AI 用上你的常用应用

让 AI 帮你找邮件、读文档、查代码,就用你已有的账号。