Skip to main content
Webhooks 使您的应用在 Fluz 平台上发生事件时实时接收通知。无需轮询,您只需注册一个 HTTPS 端点,Fluz 就会在事件发生的瞬间将数据推送给您——例如虚拟卡交易、被拒绝的购买、已完成的存款、用户通过 OAuth 链接其账户等。
本页涵盖所有 webhook 事件,跨越多个平台领域(交易活动、存款、小部件流程,以及 OAuth 账户链接),并不属于某个单一功能。
Webhooks 适用于 Fluz 平台上的所有应用类型:在您自己账户上运行的私有应用、代表其他用户通过 API 行为的公共 OAuth 应用,以及嵌入式小部件应用。

工作原理

  1. 在开发者门户为您的应用注册一个 webhook URL。
  2. 选择要监听的事件类型(或订阅全部事件)。
  3. 当匹配事件发生时,Fluz 会向您的 URL 发送带签名的 JSON 负载的 HTTP POST 请求。
  4. 您的服务器验证签名,返回一个 2xx 以确认,并处理该事件。
如果您的端点不可用或返回错误,Fluz 会在放弃投递前按指数退避最多重试5 次。

按应用类型划分的 Webhooks

事件如何路由到您的应用取决于您的应用模型。

私有应用 — 您自己的账户

Webhooks 针对您自己的账户上的活动触发。您拥有的账户上的任何交易、拒付或存款,都会触发注册在您应用上的 webhooks。 常见用例: 当虚拟卡交易被授权或清算时的通知;实时拒付告警;监控您的消费账户上的存款与余额变化。

公共应用(OAuth)— 代表用户操作

Webhooks 针对已授权您应用的账户活动触发。当用户通过 OAuth 授权您的应用时,只要用户的 OAuth 授权包含所需的 scopes,他们的事件就会被路由到您注册的 webhooks。不论用户是通过您的直接 API 集成还是嵌入式小部件进行交互,该机制都适用;关键是用户与您的应用之间存在 OAuth 关联关系。 常见用例: 知道用户何时已将其账户链接(或重新链接)到您的应用;监控已连接账户的虚拟卡支出;实时拒付通知;跟踪存款完成;接收 KYC 与 KYB 状态更新。

小部件应用

小部件应用是专门的公共应用。事件路由方式相同(基于 OAuth 关系),并且小部件应用还支持特定于小部件的事件,如转账完成与礼品卡购买。

事件类型

仅当您的应用——以及对于公共/OAuth 应用,还包括用户个体的 OAuth 授权——拥有所需的 scopes 时,Fluz 才会投递事件。没有所需 scopes 的事件(如 OAUTH_USER_LINKED)会投递给任何已订阅的公共/OAuth 应用。

交易事件

涵盖完整交易生命周期。适用于所有应用类型。

存款事件

KYC 与 KYB 事件

适用于公共/OAuth 与小部件应用。与 OAUTH_USER_LINKED 一样,这些事件没有私有应用的等价物——它们仅在通过 OAuth 关系连接的账户上触发,且仅当您的应用(以及用户)的 OAuth 授权包含所需 scope 时才会触发。

资金来源事件

仅限预发布环境FUNDING_SOURCE_STATUS_UPDATE 目前仅在预发布环境中投递。生产环境尚未发出该事件;在其上线之前,生产应用不会接收到它,届时本页会同步更新。
适用于所有应用类型。会为与您应用关联的用户投递——包括已授予 LIST_PAYMENT 的 OAuth 用户,或拥有私有应用的账户下的任何用户。 单一事件类型涵盖全部三种情形;请基于 changeType(ADDED、UPDATED、DELETED)而非事件名称进行分支处理。用户的 Fluz 现金余额不是资金来源,也永远不会产生此事件。 该事件由资金来源自身的变更触发,因此无论变更发生在何处——Fluz 移动应用、Web 应用,还是嵌入式小部件——都会触发。

OAuth 事件

适用于公共/OAuth 与小部件应用。 OAUTH_USER_LINKED 仅投递给公共/OAuth 应用类型——私有应用订阅者不会接收。由于它没有 scope 要求,凡是链接或重新链接到您应用的用户,您都会接收到该事件,而不论他们授予了哪些 scopes。使用它来开通或更新您本地的已连接用户记录、捕获授予的 scope 集,并通过 externalReferenceId 映射您自己的用户标识符。

特定于小部件的事件

适用于用户通过 Fluz 嵌入式流程进行交互的小部件与 OAuth 集成。
📘 命名说明: WIDGET_DEPOSIT_COMPLETE 与 WIDGET_WITHDRAW_COMPLETE 描述的是客户↔应用的_转账_;DEPOSIT/WITHDRAW 的措辞源于历史。将“deposit”理解为“客户 → 应用”,“withdraw”理解为“应用 → 客户”。

设置 webhooks

1. 打开开发者门户

前往 Developer Portal 并选择您的应用。

2. 打开 Webhooks 部分

  • OAuth 应用: OAuth 选项卡 → Webhook URLs。
  • 小部件应用: Widget 选项卡 → Webhook URLs。
  • API / 私有应用: 应用设置中的 Webhook URLs 部分。

3. 添加 webhook URL

点击 Add new URL 并输入您的 HTTPS 端点(例如 https://api.yourapp.com/webhooks/fluz)。

4. 选择事件

选择您希望接收的事件类型。

5. 保存

点击 Create Webhook。您的端点将立即开始接收事件。

管理 webhooks

  • 多个端点 — 每个应用可以注册多个 webhook URL。
  • 更改订阅的事件 — 删除该 webhook 并使用新的事件选择重新创建。
  • 移除 webhook — 点击其旁边的 Remove。该 webhook 会立即归档并停止接收事件。

接收 webhooks

请求格式

每个 webhook 都以 HTTP POST 发送,并包含以下请求头: 请求体为 JSON 对象,每个负载都包含一个标识事件的 eventType 字段。

端点要求

  • 仅限 HTTPS — 纯 HTTP 端点在注册时会被拒绝。
  • 可公开访问 且能接受 POST 请求。
  • 在 30 秒内响应一个 2xx。 非 2xx 响应或超时会触发重试。
  • 验证每个请求的 HMAC 签名。

验证签名

每次投递都包含 X-HMAC-Signature 请求头——它是以您应用的API key对原始 JSON 请求体计算的 HMAC-SHA256 哈希。务必在信任负载之前进行验证。
⚠️ 务必针对原始请求体进行验证。 使用 Fluz 发送的精确字节来计算 HMAC——不要对解析后的 JSON 重新序列化。重新字符串化可能会重排键或更改空白字符,导致有效签名验证失败。下面的示例为此目的捕获了原始请求体。

Node.js(Express)

Python(Flask)

响应 webhooks

您的端点必须:
  • 在 30 秒内返回一个 2xx 状态。
  • 快速返回——先确认,再异步处理。
  • 可通过 HTTPS 访问。您的端点不得:
  • 返回重定向(3xx)。
  • 对有效的 webhooks 返回 4xx/5xx(这会触发重试)。

重试策略

一旦您的端点恢复,新的事件会自动恢复投递。若需重新发送那些已耗尽重试次数的事件,请联系支持并提供相关的 X-Event-ID。

幂等与顺序

Webhooks 可能会被多次投递,并且不保证投递顺序。
  • 使用 X-Event-ID 请求头进行去重。持久化已处理的 ID(生产环境可用 Redis 或数据库),跳过已处理事件。
  • 以数据排序而非到达顺序。 若序列很重要,请按负载中的时间戳(createdAt、updatedAt、transactionDateTime)以及事件 ID 进行排序。

识别应用与用户

  • 用户: userId 是 Fluz 用户 ID。对于 OAuth/小部件事件,externalReferenceId 映射为您在 OAuth 流程中的用户标识符。
  • 应用: 交易负载包含 connectedAppId 与 connectedAppName。如果您将多个应用路由到一个端点,请基于 connectedAppId 分支处理。对于 OAUTH_USER_LINKED,应用通过 appId 标识。

负载参考

每个负载都包含一个 eventType。字段可用性可能因事件而异;为保证前向兼容,处理程序应忽略无法识别的字段。
📘 关于 status 值的说明。 对于已创建/已更新的交易,status 字段的取值为 PENDING、SETTLED 或 FAILED。被拒绝的交易携带 status 值 DECLINED(或 FAILED)。本页早期草稿曾显示 COMPLETED 作为状态——该值不会被发出;请使用 SETTLED 来判断交易已最终完成。

交易已创建(TRANSACTION_CREATE)

对任何新交易触发——虚拟卡购买、存款、转账等。

交易已更新(TRANSACTION_UPDATE)

当交易状态或详情发生变化时触发——例如,待处理的授权被清算。
字段在存在时与 TRANSACTION_CREATE 一致,另包含 updatedAt(ISO 8601)标示更改发生时间。userId、connectedAppId 与 connectedAppName 会与 TRANSACTION_CREATE 一致地传递,便于您在整个生命周期内一致地识别用户与应用。status 变为 SETTLED 表示先前的待处理交易已最终完成。 isReversal 与 TRANSACTION_CREATE 中的含义相同。见撤销 vs 退款。

交易被拒(TRANSACTION_DECLINE)

当交易被拒时触发。包含可用于驱动应用逻辑的结构化拒绝原因。
根据交易不同,可能出现其他可选字段(例如 merchantId、merchantCity、merchantState、merchantCountry、cardDisplayName、virtualCardProgram、channel、bankAccountNickname、bankAccountLastFour)。忽略您不使用的字段即可。

关联同一笔购买的事件

一笔虚拟卡购买通常会产生不止一个交易事件。有两个字段将它们关联起来:
  • referenceId 是 Fluz 购买 ID。该笔购买的授权、其清算、任何额外的清算环节,以及该笔购买的任何退回,都携带相同的值。请基于它进行分组。
  • transactionId 标识一条总账记录。待处理的授权与其清算共用同一个值,因为清算是以同一记录的 TRANSACTION_UPDATE 投递的。Fluz 单独记录的任何内容都会获得新的 transactionId。
典型购买的事件序列: 示例:一笔 8.54 的授权以 transactionId A、status: PENDING 的 TRANSACTION_CREATE 到达。商户清算了 8.56。您会收到针对 A 的 TRANSACTION_UPDATE,status: SETTLED、amount: -8.54,随后是新 transactionId B 上的 TRANSACTION_CREATE,status: SETTLED、amount: -0.02。三者都携带 referenceId: "11323677"。该笔购买的净扣款金额,是同一 referenceId 下每个 transactionId 最新金额之和:-8.56。 由此得出以下规则:
  • 以 transactionId 作为记录的键,但按 referenceId 进行对账。TRANSACTION_CREATE 携带 status: SETTLED 且此前没有待处理事件是正常现象:这是您在同一 referenceId 下已经持有的某笔购买的额外环节或退回。
  • 当授权始终未被扣款、冻结被释放时,您会收到两个事件:一个将待处理购买更新为 SETTLED 的 TRANSACTION_UPDATE,以及一个携带相同 referenceId 与正数 amount 的 Virtual Card Return。两者相互抵消为零。请将这一对事件视为对待处理金额的释放,而不是一笔已完成的购买。
  • 商户以新交易(而非针对原始授权)提交的退款所对应的 Virtual Card Return,可能不携带 referenceId。请通过 virtualCardId、amount 与商户进行匹配。

撤销 vs 退款

撤销是在资金被扣款之前释放授权冻结。持卡人的可用余额会回升,但资金从未离开,也没有资金返还。退款发生在扣款之后:资金已经流动,商户将其退回。 isReversal 用于区分两者: 请在该字段存在时优先使用它,而不是 transactionType。发卡行对这些事件的标注并不一致:某个发卡行用于商户退款的类型值,可能是另一个发卡行用于扣款前撤销的类型值,仅凭类型无法判断资金是否真的发生了移动。 Virtual Card Return 事件同样携带该字段:被释放的冻结为 true,商户退款为 false。仍有少量退回事件不携带该字段;对于这些事件,请使用 referenceId 与原始授权来加以区分。 具体哪个事件携带撤销标记,取决于被撤销的是什么:
  • 一次卡片验证作废会被记录为其自身的交易,因此会以负数 amount 的 TRANSACTION_CREATE 到达,随后再更新。这些是商户用于确认卡片有效性的小额检查,我们会立即将其作废。它们携带 isReversal: true。
  • 一次被释放的扣款前冻结,会作为一笔独立交易到达,其 transactionType 为 Virtual Card Return,amount 为正数,并携带该笔购买的 referenceId。原始购买会同时被更新为 SETTLED,因此两者相互抵消为零。无论冻结是由 Fluz 还是发卡行释放,该退回都携带 isReversal: true。
  • 商户退款同样以 transactionType 为 Virtual Card Return 到达,并携带 isReversal: false。当 referenceId 存在时,请据此与原始购买匹配;否则请通过 virtualCardId、amount 与商户进行匹配。
撤销不是失败,但仍需要处理:请释放您为匹配授权记录的冻结,否则它会在您这边永远保持打开状态。不要将其记为新的资金流出,因为并没有发生资金移动。

对账金额

请以 amount 进行对账。它是该事件中实际移动的美元金额,因此同一 referenceId 下,按 transactionId 取最新的 amount 并求和,无论该笔购买有多少个清算环节或退回,都能得到其净额。 originalCurrencyAmount 不能这样累加。它是发卡行针对该事件报告的请求金额。在一次退回事件中,它是该次退回之前仍处于冻结状态的金额,因此它重复了此前事件已经携带过的内容,对其求和会造成重复计算。 示例:一笔 39.69 的授权,其冻结分两步释放——例如订单的一部分缺货,其余部分随后被释放。 amount 的值净额为 0.00,因为没有发生任何扣款。originalCurrencyAmount 的值描述的是每一步处于冻结状态的金额,而不是实际移动的资金。

存款完成(DEPOSIT_COMPLETE)

当从资金来源到消费账户的存款完成时触发。

OAuth 用户已链接(OAUTH_USER_LINKED)

当用户完成与您应用的 OAuth 链接时触发——包括首次链接与后续更新(例如用户以不同 scope 集重新授权)。仅适用于公共/OAuth 与小部件应用。接收此事件不需要任何 scope。
由于 OAUTH_USER_LINKED 在重新链接/更新时也会触发,请将其视为 upsert:首次接收时创建已连接用户,后续投递时刷新已存储的 scope 集。

消费账户访问权限 [#spend-account-access]

用户链接到您的应用时,权限步骤允许其授予对全部消费账户的访问,或仅授予所选的消费账户。spendAccountAccess 告诉您该链接最终属于哪一种,这样您就可以在链接完成后立即对消费账户进行操作——例如调用 getSpendAccountPaymentInstructions 生成入账指引——而无需额外查询。
  • 受限授权。 restricted 为 true,userCashBalanceIds 列出用户在链接时选择的消费账户。这些 ID 目前仅在 staging 环境投递,生产环境尚不包含。includesFutureSpendAccounts 告诉您该列表是否固定:
    • false——该授权恰好覆盖这些消费账户。只有用户重新链接时列表才会变化,而重新链接会发送新的 OAUTH_USER_LINKED 事件。
    • true——运营方允许用户包含之后开设的消费账户。每个新开设的消费账户都会自动加入该授权,但开设账户不会触发此事件,因此 userCashBalanceIds 仅在链接时刻是准确的。请使用用户的访问令牌调用 getWallet 获取当前列表。
  • 不受限授权。 restricted 为 false,userCashBalanceIds 为 null。该授权覆盖用户当前拥有的以及之后开设的所有消费账户。Fluz 不会在负载中枚举当前消费账户,因为开设新消费账户不会触发此事件,快照会过时。请使用用户的访问令牌调用 getWallet 获取当前列表。
仅包含 ID。不包含昵称和余额,因为它们可能在不触发此事件的情况下发生变化——请从 getWallet 读取,其 balances.userCashBalances 已按链接所允许的消费账户过滤。 每当授权发生变化时,该事件会携带更新后的值再次触发:用户重新授权、授予的 scopes 变化,或在 scopes 不变的情况下授予的消费账户变化(例如用户将一个消费账户换成另一个)。请将最新一次投递视为权威值,并替换(而非合并)已存储的 ID 列表。 请将此字段视为一种便利。Webhook 投递可能延迟或乱序到达,因此如果您需要确定某个链接允许哪些消费账户,请调用 getWallet——它才是事实来源。
仅限 stagingOAUTH_USER_LINKED 上的 spendAccountAccess 目前仅在 staging 环境投递。生产环境的投递尚不包含该字段;上线后将更新本页面。

KYC 发起(WIDGET_KYC_INITIATION)

当用户开始身份验证时触发。仅适用于公共/小部件应用。

转账完成 — 客户到应用(WIDGET_DEPOSIT_COMPLETE)

当用户向您的应用转账资金时触发。

转账完成 — 应用到客户(WIDGET_WITHDRAW_COMPLETE)

当您的应用向用户转账资金时触发。

礼品卡购买(WIDGET_PURCHASE_GIFT_CARD)

当通过小部件完成一次礼品卡购买时触发。
常见的小部件负载字段: userId(Fluz 用户 ID)、accountId(Fluz 账户 ID)、externalReferenceId(您在 OAuth 流程中的用户标识符),以及在适用时的 amount。

KYC 状态更新(KYC_STATUS_UPDATE)

当已连接用户的 KYC 状态发生变化时触发。仅适用于公共/OAuth 与小部件应用;需要 VERIFY_KYC scope。每个负载共享一个通用外层;verificationData 对象的结构由 verificationType 区分,见下方各类型示例。
当状态变化源于基于 SSN 的验证时触发。
当存在时(仅 SSN 与 DOCUMENTS 类型),legalAddress 的结构为 { streetAddressLine1, streetAddressLine2 (optional), city, state, postalCode, country }。

KYB 状态更新(KYB_STATUS_UPDATE)

当已连接企业的 KYB 状态发生变化时触发。仅适用于公共/OAuth 应用;需要 REGISTER_BUSINESS scope。

资金来源状态更新(FUNDING_SOURCE_STATUS_UPDATE)

当用户新增、重命名或移除一个资金来源——银行账户、银行卡,或 PayPal 账户——时触发。需要 LIST_PAYMENT scope。目前仅限预发布环境——尚未在生产环境中投递。
仅当此负载中的字段确实发生变化时,UPDATED 才会触发——重命名来源或状态变更会产生一次;常规的内部记录维护不会。 移除一个资金来源不会删除该记录,而是将其标记为非活跃,因此 DELETED 事件携带的是终态 status(INACTIVE、DISABLED 或 REMOVED),而非记录的缺失。请依赖 changeType 来判断某个来源已被移除。 此负载中永远不会包含完整的卡号或银行账号。

最佳实践

  • 快速响应,稍后处理。 立即返回 200,并异步处理事件,避免超时与不必要的重试。
  • 验证每个请求。 使用您的 API key 针对原始请求体验证 X-HMAC-Signature。
  • 使用事件 ID 去重。 跟踪 X-Event-ID 以处理重试/重复投递。
  • 将类枚举字段视为开放字符串。 transactionType、channel 与 declineCategory 的新值可能随时间出现;仅对您关注的值分支,并容忍未知值。
  • 对 OAUTH_USER_LINKED 执行 upsert。 它对每个用户可能触发多次;每次都刷新已存储的 scope 集和 spendAccountAccess,而非假设仅首次链接。
  • 按 fundingSourceId 为资金来源建立键。 单一事件类型涵盖全部三种变更类型;请基于 changeType 分支处理,并按 changedAt 对同一来源的事件排序。
  • 接受未知字段。 负载可能随时间增加字段;忽略无法识别的字段,而不是失败。
  • 监控您的端点。 对连续的非 2xx 响应进行告警——在 5 次失败尝试后,事件的投递会被放弃。

故障排查

需要帮助?

  • 技术问题: 检查您的端点日志,并将 X-Event-ID 提供给支持团队。
  • Scope 相关问题: 参见应用 Scopes 和拒绝代码。