Skip to content

Webhooks -- 概述

Webhooks 允许您的应用实时接收 Owem Pay 平台上的事件通知。当事件发生时,Owem Pay 会向您注册的 URL 发送 HTTP POST 请求。

工作原理

  1. 在您的账户中注册 Webhook URL
  2. 当事件发生时(如:收到 PIX),Owem Pay 向您的 URL 发送 HTTP POST
  3. 您的应用处理通知并返回 2xx 状态码(200、201 或 204)

可用事件

Owem Pay 仅投递与 PIX 相关的事件。其他产品(boleto、账户、STA、非 PIX 转账)不在范围内。任何尝试订阅下表之外事件的请求都会被拒绝,返回 events: contains invalid events: ...

事件状态 body描述触发
pix.charge.createdcreatedQR 码已生成或 cash-in 已启动激活
pix.charge.paidpaidPIX 已接收并结算激活
pix.charge.expiredexpiredQR 码在未付款情况下过期(worker 每 5 分钟检查一次)激活
pix.charge.cancelledcancelledQR 码在付款前取消已注册,尚未触发
pix.payout.queuedqueued已发送的 PIX 由于速率限制(每 merchant 的 ClientLimiter 或 DICT BACEN bucket)入队。约每 3 秒自动重试,最大 TTL 2 小时激活
pix.payout.processingprocessingPIX 已发送,等待 BACEN 确认激活
pix.payout.confirmedsettledPIX 已发送并确认(终止激活
pix.payout.failedrejectedPIX 已发送,被 SPI 拒绝(终止激活
pix.payout.returnedreturned已发送的 PIX 已退回激活
pix.refund.requestedrequested收到退款请求(BACEN 违规);在客户余额上创建预防性封锁激活
pix.refund.completedsettled / completed抗辩分析完成,退款已执行(或已释放)激活
pix.return.receivedsettled收到 PIX 退款(贷记)激活
pix.infraction.createdACKNOWLEDGED对方通过 BACEN DICT 报告 PIX 违规;需要抗辩或自动生成预防性封锁(>R$1k)激活
pix.infraction.resolvedCLOSED / CANCELLED违规已解决(admin 关闭、auto-deny 或对方取消)激活
pix.infraction.defense_submitteddefense_submittedmerchant(门户或 API)已提交抗辩;等待 BACEN 分析激活
webhook.testtest手动测试。仅通过 Admin/Merchant portal 可用 — External API 不暴露触发测试的端点手动触发(非 External API)

pix.charge.cancelled 尚未触发

事件在 enum 中且可订阅,但系统今天没有 QR 码取消流程。如果您订阅,POST /webhooks 正常响应 201 — 但不会收到通知。继续监控 pix.charge.expired 以了解 QR 的自然生命周期。

安全性

每个通知包含用于验证的安全和识别头:

描述
X-Owem-Signature载荷的 HMAC-SHA256 签名(前缀 sha256=)。在罕见情况下(注册时没有 secret 的 webhook),文字值为 unsigned — 参见下方注释
X-Owem-Timestamp发送的 Unix 时间戳(秒)
X-Owem-Event-Id投递的唯一 UUID(用于去重)
X-Owem-Event-Type事件类型(例:pix.charge.paid
Content-Type始终为 application/json
User-Agent始终为 Owem-Webhook/1.0 — 用于防火墙/WAF 白名单。未来演变将遵循 Owem-Webhook/{version} 模式;如果您想对新版本免疫,按前缀 Owem-Webhook/ 过滤

当 webhook 没有 secret 时签名为 unsigned

如果 webhook 是在没有 secret 字段(遗留场景)的情况下注册的,头 X-Owem-Signature 的字面值为 unsigned。这在您那边禁用 HMAC 验证。实际上,当字段省略时(自 session 80 起),POST /api/external/webhooks 生成 64 字符的随机 secret,因此该场景仅出现在非常旧的注册或通过 admin bypass 中。如果您收到 unsigned,请使用显式 secret 注册新 webhook 并移除旧的。

Webhook 中的 SHA256 vs API 中的 SHA512

API 使用 HMAC-SHA512 验证您发送的请求。Owem Pay 发送的 webhook 在 X-Owem-Signature 签名中使用 HMAC-SHA256。它们是不同的算法 -- 各有其上下文。

验证签名

验证签名以确保通知由 Owem Pay 发送:

javascript
const crypto = require('crypto');

function validateWebhook(rawBody, timestamp, signature, secret) {
  // rawBody is the RAW request body string (before any JSON parse)
  // timestamp is unix seconds (e.g., 1712160000)
  const message = `${timestamp}.${rawBody}`;
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(message)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  );
}

使用 RAW body,不要重新序列化

必须使用 HTTP 请求的 body,即字节到达您应用时的那一份。如果您先执行 JSON.parse 然后再执行 JSON.stringify,产生的字节不会与 Owem 用于签名的字节完全一致,验证将失败。

在 Express/Node 中:使用 express.raw({ type: 'application/json' }) 或在任何 parse 中间件之前保存 body。

在其他框架中:配置在 JSON 中间件之前捕获 raw body。

WEBHOOK 中的键排序:NÃO 需要

对于 webhook 验证(HMAC-SHA256),您不需要按字母顺序排列键 — 使用从 Owem HTTP 请求中接收的 raw body。

⚠️ 注意 — 与请求发送的差异:在您发送的请求的 HMAC-SHA512 签名中,按字母顺序的键是必须的(Owem 服务器在验证前重新排序)。不要混淆这两种场景:

  • 收到的 Webhook(HMAC-SHA256):验证 raw body,无需重新排序
  • 发送的 Request(HMAC-SHA512):签名前按字母顺序排列您的键

始终验证

切勿在未验证签名的情况下处理 webhook。这可以防止伪造请求。

此外,验证 X-Owem-Timestamp 是否在当前时间的 ± 5 分钟之内(防重放保护 — 服务器默认拒绝"旧"webhook;此检查应由您的端点作为防御深度执行)并按 X-Owem-Event-Id 去重事件(针对重试的保护)。

重试策略

如果您的 URL 返回非 2xx 状态(或 30 秒后超时),Owem Pay 将执行最多 8 次重试,采用指数退避策略。第一次和第八次尝试之间的总时间约为 7h45min

尝试与上次尝试的间隔累计时间
第 1 次—(立即,通过 Task.start 主动触发)~50–200 ms
第 2 次30 秒~30 s
第 3 次2 分钟~2,5 min
第 4 次10 分钟~12,5 min
第 5 次30 分钟~42,5 min
第 6 次1 小时~1,75 h
第 7 次2 小时~3,75 h
第 8 次4 小时~7,75 h

8 次尝试均失败后,webhook_delivery 被标记为 failed,不再自动重发。您可以向 Owem 支持请求手动重放,提供 X-Owem-Event-Id(或让有 admin 访问权限的操作员通过门户重放)。

delivery 状态

每个 delivery 经过以下状态:pending(已创建,等待投递)→ delivered(收到 2xx)或 failed(8 次尝试耗尽)或 expired(重放保护)。

关于 expired 当 Oban worker 即将处理第一次尝试且 delivery 自创建(inserted_at)已超过 5 分钟时,发送被中止,状态直接变为 expired。这防止了晚期重新处理(由于队列积累、pod 重启等)触发旧事件的通知。请求 Owem 支持的手动重放通过内部 flag manual_replay: true 传递,绕过此 guard — 客户端正常接收通知。

持久性

在尝试第一次投递之前,事件被持久化到 PostgreSQL 的 webhook_deliveries 表中。如果触发 webhook 的 pod 在投递期间崩溃,Oban 在下次重试时自动恢复 — 不会丢失任何事件。

幂等性

您的应用应具备幂等性:如果多次收到同一事件(通过 X-Owem-Event-Id 识别),应在不产生重复效果的情况下处理。

通过 admin 手动重放

如果 delivery 失败且您需要重新发送,Owem 团队可以通过 admin dashboard 执行手动重放。请联系支持并提供 delivery 的 event_id

重复投递(已知的 race condition)

系统使用"eager delivery"机制加速第一次投递 + Oban worker 作为持久重试回退。在高并发场景中,这两个路径可能并行触发同一个 webhook(~1 秒的窗口)。在这种情况下,您通过 HTTP 收到相同的载荷 2 次,但具有相同的 X-Owem-Event-Id — 是同一个事件,不是重试。

为避免重复影响:

  • X-Owem-Event-Id 去重(推荐 — 每个 delivery 唯一的 UUID,在重试和上述 race condition 中稳定)
  • 或者在事件合适时按 end_to_end_id + event_type 去重

这是预期行为,不是错误。合法的重试(5xx/超时后)也重用相同的 X-Owem-Event-Id

Webhook 中的 External ID

当交易创建时包含 external_id,该字段会包含在 webhook 载荷的 data 对象中。使用它将事件与您系统中的订单关联,无需额外查询。

端点要求

  • URL 必须使用 HTTPS(除非注册时设置 allow_insecure: true
  • 必须在 30 秒内返回 2xx 状态(在投递 worker 中配置的 receive_timeout
  • 响应 body 被忽略
  • 建议快速响应(立即 200 OK)并在您这边异步处理事件;长延迟会降低吞吐量并增加重试机会

下一步

Owem Pay Instituição de Pagamento — ISPB 37839059