Community编程与开发github.com

NombaOSS/nomba-webhooks

构建安全的Nomba Webhook端点,验证签名、去重投递并正确处理每种事件(payment_success、payment_failed、payment_reversal、payout_success、payout_failed、payout_refund)。适用于用户需要接收、处理、验证或调试Nomba Webhooks(或来自Nomba的“通知”/“回调”)的场景,支持 Express、Next.js、NestJS、Fastify、Flask、FastAPI、Django、Laravel、Symfony 或纯 PHP,包括当用户仅表示“在服务器上确认支付”、“监听成功转账”或粘贴带有 event_type 和 nomba-signature 头部的负载时。也适用于审查现有的 webhook 处理器。

nomba-webhooks 是什么?

此技能为 Nomba 支付平台提供标准化的 webhook 处理流程。它确保每个传入的 webhook 请求都经过签名验证,以防止伪造事件,并通过 requestId 去重来应对 Nomba 自动重试导致的重复投递。该技能覆盖了所有关键事件类型(支付成功/失败、冲正、打款成功/失败/退款),并为每种事件提供了明确的操作指南,包括匹配记录、检查金额和货币,以及正确处理退款以避免双重支付。它支持 Node.js、Python、PHP 等多种主流框架,并提供了分步实施清单、代码示例和测试指南,帮助开发者快速集成一个安全可靠的 Nomba webhook 端点。

兼容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/NombaOSS/skills/tree/HEAD/skills/nomba-webhooks

Installed? Explore more 编程与开发 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

Nomba Webhooks

Webhook 是 Nomba 通知你的服务器资金实际发生转移的方式。互联网上的任何人都可以向你的 Webhook URL 发送 POST 请求,因此处理器必须先证明每个请求确实来自 Nomba,然后才能将订单标记为已支付或进行退款。同时,它还必须处理 Nomba 可能重试同一事件的情况。

使用官方 SDK 的验证器;不要手动实现 HMAC。Nomba 的签名覆盖了以 : 连接特定字段,而且 SDK 还能防止简单实现可能遗漏的重放攻击。

语言验证方法
Node.js / TS (@nombaoss/nomba-node)verifyWebhook(rawBody, headers, signingKey)
Python (nomba)from nomba.webhooks import verify_signature → verify_signature(raw_body, headers, signing_key)
PHP (nombaoss/nomba-php)Nomba\Webhook::verify($rawBody, $headers, $signingKey)

每种语言的确切导入和参数类型请参考 references/sdk-setup.md。每个方法都会返回解析后的事件对象,或者在验证失败时抛出异常(WebhookVerificationError / WebhookVerificationException)。它们都不需要 API 凭证,只需要从 Nomba 后台(开发者 → Webhook 设置)获取的 webhook 签名密钥。请通过环境变量(例如 NOMBA_WEBHOOK_SIGNING_KEY)读取它。这是一个与 API 客户端密钥不同的独立密钥。

处理器分步指南

  1. 获取原始请求体。 验证 Nomba 发送的精确字节。如果先运行了 JSON 主体解析器,然后你又对对象进行了重新序列化,签名可能不再匹配,或者更糟,你验证的东西和实际处理的东西不一致。每个框架获取原始请求体的方式都不同;请参考 references/frameworks.md 获取正确的方法,并在框架启用 CSRF 保护时将该路由排除在外。
  2. 验证,然后解析。 使用验证器返回的事件对象。验证失败时,返回 401(或 400)状态码并停止处理。不要在 info 级别记录被拒绝请求的完整主体或头部信息。
  3. 对 requestId 进行去重。 Nomba 会重试投递,因此同一事件可能多次到达。使用唯一约束记录每个已处理的 requestId,跳过已经见过的。即使对重复事件也要返回 200,否则 Nomba 会持续重试。
  4. 快速确认。 迅速返回 200,并将耗时的工作(如发送邮件、订单履行、账本更新)放在以 requestId 为键的后台作业中执行。慢速处理器会导致超时和重新投递。
  5. 根据下表处理事件。 通过引用标识符将事件与你自己的记录匹配,并在履行订单前检查金额和货币是否符合预期。

每个事件的含义

event_type匹配依据操作
payment_success结账: data.order.orderReference;虚拟账户/转入: data.transaction.aliasAccountReference 或 transactionId在检查 data.transaction.transactionAmount(和货币)与订单匹配后,标记为已支付。对于令牌化结账,将 data.tokenizedCardData.tokenKey(如果存在且不为 "N/A")与客户关联,并仅存储在服务端。
payment_failed同上标记尝试失败;允许客户重试。未产生任何扣款。
payment_reversal同上你收到的款项已被撤回。撤销订单履行或标记为审核。
payout_successdata.transaction.merchantTxRef标记转账或账单支付已完成。
payout_faileddata.transaction.merchantTxRef不要立即退款。 Nomba 可能通过其他路由重试。将其标记为失败并等待。
payout_refunddata.transaction.merchantTxRef资金已返回你的 Nomba 钱包。现在可以安全地向客户退款或贷记,并标记该打款为失败。

在 payout_failed(或超时)时而非 payout_refund 时退款,是商户支付双倍费用的经典错误。对于任何无法识别的事件,记录日志并返回 200。

通过 API 确认资金决策。 Nomba 的签名只覆盖了 event_type、requestId、data.merchant.userId、data.merchant.walletId、data.transaction.transactionId、type、time、responseCode 和时间戳。金额、orderReference、merchantTxRef 和卡片 tokenKey 并未签名。因此,在履行、退款或存储卡片令牌之前,请使用 SDK 重新查询并根据 API 的响应进行操作:对于结账支付使用 transactions.get({ orderReference })(生产环境也使用 checkout.getTransaction),对于打款使用 transactions.get({ merchantTxRef })。通过对签名后的 requestId 进行去重,可以阻止已处理事件的篡改副本,而重新查询则覆盖其余风险。

重放保护

对 requestId 进行去重是主要的防御手段:如果重放或重试投递了你已处理过的事件,不会产生任何影响。

验证器还可以拒绝旧的时间戳(toleranceSeconds / tolerance_seconds / PHP 中的第四个参数),但默认情况下关闭此功能。Nomba 文档将 nomba-timestamp 描述为负载发送的时间,但没有说明自动重试是否会携带一个新的时间戳。如果它们保留了原始时间戳,那么短于 Nomba 重试窗口的容差将会拒绝中断后的合法重试,导致已支付的订单永远无法被确认。仅当在沙箱环境中确认了重新投递会携带新时间戳后,才启用此功能,并且要保持一个宽松的容差(例如一小时),同时确保服务器时钟同步。

示例(Node.js + Express)

import express from "express";
import { verifyWebhook, WebhookEventType } from "@nombaoss/nomba-node";

const app = express();

// 仅为此路由获取原始请求体;在应用级 express.json() 之前注册它。
app.post("/webhooks/nomba", express.raw({ type: "*/*" }), async (req, res) => {
  let event;
  try {
    event = verifyWebhook(req.body, req.headers, process.env.NOMBA_WEBHOOK_SIGNING_KEY!);
  } catch {
    return res.sendStatus(401);
  }

  // 对 request_id 添加唯一约束;如果已见过则返回 false。
  const isNew = await webhookEvents.recordOnce(event.requestId, event.event_type);
  if (isNew) await jobs.enqueue("nomba-webhook", event); // 在后台处理
  res.sendStatus(200);
});

Python(Flask/FastAPI/Django)、PHP(Laravel/纯 PHP)和其他 Node.js 框架示例见 references/frameworks.md。

测试

  • 使用 SDK 文档中的测试向量对处理器进行单元测试:请求体来自 SDK 的 webhook-fixture.json,密钥为 SampleSignatureKey123,头部为 nomba-timestamp: 2025-09-29T10:51:44Z,nomba-signature: q30yFZDyykoymBf9+mt1wz5UAuLk57raPGPHlGzIv00=。断言错误的密钥、被篡改的请求体和缺失的头部会被拒绝,并且重复的 requestId 只会被处理一次。
  • 端到端测试:在后台中设置一个测试用的 webhook URL 和签名密钥,使用隧道工具暴露你的本地服务器,并创建沙箱支付。后台可以重新推送或调试投递记录 (https://developer.nomba.com/docs/api-basics/troubleshoot-webhooks)。

检查清单

  • 使用 SDK 验证原始请求体,在任何解析或业务逻辑之前。
  • 签名密钥来自环境变量,而非硬编码。
  • 签名无效时返回 401/400;不执行任何其他操作。
  • 通过唯一约束对 requestId 进行去重;重复事件仍然返回 200。
  • 耗时工作在后台执行;处理器快速返回 200。
  • 履行订单前,根据订单核对金额和货币。
  • 仅在 payout_refund(或重新查询返回 REFUND)时进行退款。
  • 该路由已排除 CSRF 保护,并使用 HTTPS。

相关技能