本页涵盖所有 webhook 事件,跨越多个平台领域(交易活动、存款、小部件流程,以及 OAuth 账户链接),并不属于某个单一功能。
工作原理
- 在开发者门户为您的应用注册一个 webhook URL。
- 选择要监听的事件类型(或订阅全部事件)。
- 当匹配事件发生时,Fluz 会向您的 URL 发送带签名的 JSON 负载的 HTTP
POST请求。 - 您的服务器验证签名,返回一个
2xx以确认,并处理该事件。
按应用类型划分的 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 都以 HTTPPOST 发送,并包含以下请求头:
请求体为 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获取当前列表。
getWallet 读取,其 balances.userCashBalances 已按链接所允许的消费账户过滤。
每当授权发生变化时,该事件会携带更新后的值再次触发:用户重新授权、授予的 scopes 变化,或在 scopes 不变的情况下授予的消费账户变化(例如用户将一个消费账户换成另一个)。请将最新一次投递视为权威值,并替换(而非合并)已存储的 ID 列表。
请将此字段视为一种便利。Webhook 投递可能延迟或乱序到达,因此如果您需要确定某个链接允许哪些消费账户,请调用 getWallet——它才是事实来源。
仅限 staging
OAUTH_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
- Documents
- System
当状态变化源于基于 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 次失败尝试后,事件的投递会被放弃。