> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fluz.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks（网页钩子）

Webhooks 使你的应用在 Fluz 平台上发生事件时实时接收通知。无需轮询，你只需注册一个 HTTPS 端点，Fluz 就会在事件发生的瞬间将数据推送给你——例如虚拟卡交易、被拒绝的购买、完成的存款等。

<Note>
  本页作为 Webhooks 的顶层章节，因为 webhook 事件跨越多个平台领域（小部件流程与交易活动），而不隶属于单一功能。
</Note>

Webhooks 适用于 Fluz 平台上的所有应用类型：在你自己账户上运行的私有应用、通过 API 代表其他用户操作的公共 OAuth 应用，以及嵌入式小部件应用。

## 工作原理

1. 你在开发者门户为你的应用注册一个 webhook URL。
2. 选择要监听的事件类型（或订阅全部事件）。
3. 当匹配的事件发生时，Fluz 会向你的 URL 发送带签名的 JSON 负载的 HTTP `POST`。
4. 你的服务器验证签名、以 `2xx` 确认并处理事件。

```mermaid theme={null}
sequenceDiagram
    participant Platform as Fluz Platform
    participant Dispatcher as Webhook Dispatcher
    participant You as Your Endpoint

    Platform->>Dispatcher: Event occurs
    Dispatcher->>Dispatcher: Match subscriptions and verify scopes
    Dispatcher->>Dispatcher: Sign payload with HMAC
    Dispatcher->>You: HTTP POST with JSON payload
    You-->>Dispatcher: 200 OK
```

如果你的端点不可用或返回错误，Fluz 会采用指数退避最多重试**5 次**，然后将事件标记为 `FAILED`。

## 按应用类型的 Webhooks

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

### 私有应用 — 你的自有账户

Webhooks 针对**你自己的账户**活动触发。你所拥有账户上的任何交易、拒绝或存款，都会触发注册在你应用上的 webhooks。

**常见用例：** 当虚拟卡交易被授权或结算时的通知；实时拒绝警报；监控你的消费账户上的存款与余额变动。

### 公共应用（OAuth）— 代表用户操作

Webhooks 针对**已授权你的应用的账户**活动触发。当用户通过 OAuth 授权你的应用时，其事件会被路由到你注册的 webhooks——前提是用户的 OAuth 授权包含所需的权限范围（scopes）。无论用户是通过你的直接 API 集成还是嵌入式小部件交互都适用；关键在于用户与应用之间存在 OAuth 关系。

**常见用例：** 监控已连接账户的虚拟卡消费；实时拒绝通知；跟踪存款完成；接收 KYC 状态更新。

### 小部件应用

小部件应用是一类特殊的公共应用。事件的路由方式相同（基于 OAuth 关系），并且小部件应用还支持小部件特定事件，如转账完成与礼品卡购买。

## 事件类型

仅当你的应用——以及对于公共/OAuth 应用，单个用户的 OAuth 授权——拥有所需的权限范围时，Fluz 才会投递事件。

### 交易事件

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

| Event                 | Description              | Required scopes                  |
| --------------------- | ------------------------ | -------------------------------- |
| `TRANSACTION_CREATE`  | 创建了新的交易（例如，虚拟卡购买、存款、转账）。 | `LIST_PAYMENT`, `LIST_PURCHASES` |
| `TRANSACTION_UPDATE`  | 交易的状态或金额发生变更（例如，结算、调整）。  | `LIST_PAYMENT`, `LIST_PURCHASES` |
| `TRANSACTION_DECLINE` | 交易被拒绝（例如，资金不足、卡控制）。      | `LIST_PAYMENT`, `LIST_PURCHASES` |

### 存款事件

| Event              | Description       | Required scopes |
| ------------------ | ----------------- | --------------- |
| `DEPOSIT_COMPLETE` | 从资金来源到消费账户的存款已完成。 | `MAKE_DEPOSIT`  |

### 小部件特定事件

适用于用户通过 Fluz 嵌入式流程交互的小部件与 OAuth 集成。

| Event                       | Description     | Required scopes                |
| --------------------------- | --------------- | ------------------------------ |
| `WIDGET_KYC_INITIATION`     | 用户发起身份验证（KYC）。  | `VERIFY_KYC`                   |
| `WIDGET_DEPOSIT_COMPLETE`   | 从客户到你的应用的转账已完成。 | `MAKE_PAYOUT_TRANSFER_SEND`    |
| `WIDGET_WITHDRAW_COMPLETE`  | 从你的应用到客户的转账已完成。 | `MAKE_PAYOUT_TRANSFER_RECEIVE` |
| `WIDGET_PURCHASE_GIFT_CARD` | 礼品卡购买已完成。       | `PURCHASE_GIFTCARD`            |

> 📘 **命名说明：** `WIDGET_DEPOSIT_COMPLETE` 与 `WIDGET_WITHDRAW_COMPLETE` 描述的是客户↔应用的\_转账\_；`DEPOSIT`/`WITHDRAW` 的措辞来自历史。将 “deposit” 视为 “客户 → 应用”，将 “withdraw” 视为 “应用 → 客户”。

## 设置 webhooks

### 1. 打开开发者门户

前往 [Developer Portal](https://app.fluz.app/for-developers) 并选择你的应用。

### 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` 发送，并包含以下请求头：

| Header             | Description                                        |
| ------------------ | -------------------------------------------------- |
| `Content-Type`     | 始终为 `application/json`。                            |
| `X-HMAC-Signature` | 使用你应用的 API key 对原始 JSON 请求体进行 HMAC-SHA256 签名所得的签名。 |
| `X-Event-ID`       | 此事件的唯一 UUID——用它来去重。                                |

请求体为 JSON 对象，每个负载都包含标识事件的 `eventType` 字段。

### 端点要求

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

## 验证签名

每次投递都会包含 `X-HMAC-Signature` 请求头——这是对**原始** JSON 请求体，使用你应用的**API key**计算的 HMAC-SHA256。务必在信任负载之前进行验证。

> ⚠️ **基于原始请求体进行验证。** 使用 Fluz 发送的精确字节计算 HMAC——**不要**对解析后的 JSON 重新序列化。重新字符串化可能会重排键或改变空白，导致有效签名验证失败。下列示例都捕获原始请求体，正是出于此原因。

### Node.js (Express)

```javascript theme={null}
const express = require('express');
const crypto = require('crypto');
const app = express();

// Capture the raw body so the HMAC is computed over the exact bytes Fluz sent.
app.use('/webhooks/fluz', express.raw({ type: 'application/json' }));

function verifyFluzWebhook(rawBody, signature, apiKey) {
  const expected = crypto.createHmac('sha256', apiKey).update(rawBody).digest('hex');
  const received = Buffer.from(signature || '', 'hex');
  const computed = Buffer.from(expected, 'hex');
  return received.length === computed.length &&
         crypto.timingSafeEqual(received, computed);
}

app.post('/webhooks/fluz', (req, res) => {
  const signature = req.headers['x-hmac-signature'];
  const eventId = req.headers['x-event-id'];

  // req.body is a Buffer because of express.raw above
  if (!verifyFluzWebhook(req.body, signature, process.env.FLUZ_API_KEY)) {
    return res.status(401).send('Invalid signature');
  }

  // Acknowledge immediately, then process asynchronously
  res.status(200).send('OK');

  const event = JSON.parse(req.body.toString('utf8'));
  handleEvent(eventId, event).catch(err =>
    console.error('Webhook processing error:', err)
  );
});
```

### Python (Flask)

```python theme={null}
import hmac, hashlib
from flask import Flask, request

app = Flask(__name__)

def verify_fluz_webhook(raw_body: bytes, signature: str, api_key: str) -> bool:
    expected = hmac.new(api_key.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature or "")

@app.post("/webhooks/fluz")
def fluz_webhook():
    signature = request.headers.get("X-HMAC-Signature")
    event_id = request.headers.get("X-Event-ID")

    # request.get_data() returns the raw bytes — do NOT use request.json for verification
    if not verify_fluz_webhook(request.get_data(), signature, FLUZ_API_KEY):
        return "Invalid signature", 401

    enqueue(event_id, request.get_json())  # process asynchronously
    return "OK", 200
```

## 响应 webhooks

你的端点**必须**：

* 在 30 秒内以 `2xx` 状态响应。
* 快速返回——先确认，再异步处理事件。
* 可通过 HTTPS 访问。

你的端点**不得**：

* 返回重定向（`3xx`）。
* 对有效的 webhooks 返回 `4xx`/`5xx`（这会触发重试）。

## 重试策略

| Behavior               | Detail                  |
| ---------------------- | ----------------------- |
| Max attempts           | 5                       |
| Schedule               | 指数退避                    |
| Trigger                | 任何非 `2xx` 响应或超时         |
| Timeout                | 每次尝试 30 秒               |
| After all retries fail | 事件被标记为 `FAILED`；不再继续投递。 |
| Logging                | 每次尝试都会单独记录，便于审计。        |

一旦你的端点恢复，新的事件将自动恢复投递。若要重新发送已达到 `FAILED` 的事件，请联系支持并提供相关的 `X-Event-ID`。

### 事件状态生命周期

| Status           | Meaning               |
| ---------------- | --------------------- |
| `CREATED`        | 事件已记录并入队。             |
| `IN_PROGRESS`    | 正在匹配 webhook 订阅。      |
| `NO_SUBSCRIBERS` | 没有活动的 webhooks 匹配此事件。 |
| `SUCCESS`        | 投递成功。                 |
| `FAILED`         | 已耗尽所有重试。              |

## 幂等性与顺序

Webhooks 可能会被**多次投递**，并且**不保证投递顺序**。

* 使用 `X-Event-ID` 请求头进行**去重**。持久化已处理的 ID（生产环境使用 Redis 或数据库），跳过已处理的事件。
* **按数据排序，而非到达顺序。** 如果顺序很重要，请按照负载时间戳（`createdAt`、`updatedAt`、`transactionDateTime`）和事件 ID 排序。

```javascript theme={null}
const seen = new Set(); // use Redis or a database in production

async function handleEvent(eventId, event) {
  if (seen.has(eventId)) return;  // already processed — skip
  seen.add(eventId);

  switch (event.eventType) {
    case 'TRANSACTION_CREATE':    await onTransactionCreated(event); break;
    case 'TRANSACTION_UPDATE':    await onTransactionUpdated(event); break;
    case 'TRANSACTION_DECLINE':   await onTransactionDeclined(event); break;
    case 'DEPOSIT_COMPLETE':      await onDepositComplete(event); break;
    case 'WIDGET_KYC_INITIATION': await onKycInitiated(event); break;
    // ...handle remaining widget events
  }
}
```

## 识别应用与用户

* **用户：** `userId` 是 Fluz 用户 ID。对于 OAuth/小部件事件，`externalReferenceId` 映射为你在 OAuth 流程中的用户标识符。
* **应用：** 交易负载包含 `connectedAppId` 与 `connectedAppName`。如果你将多个应用路由到同一个端点，请基于 `connectedAppId` 分支处理。

## 负载参考

每个负载都包含 `eventType`。字段可用性因事件而异；为前向兼容，处理程序应忽略未识别字段。

### 交易已创建（`TRANSACTION_CREATE`）

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

```json theme={null}
{
  "eventType": "TRANSACTION_CREATE",
  "recordId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "transactionId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "accountId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
  "userId": "550e8400-e29b-41d4-a716-446655440000",
  "transactionType": "PURCHASE",
  "amount": 42.99,
  "destination": "Coffee Shop",
  "source": "Virtual Card",
  "status": "COMPLETED",
  "channel": "VIRTUAL_CARD",
  "connectedAppId": "your-app-id",
  "connectedAppName": "Your App",
  "createdAt": "2025-01-15T10:30:00.000Z",
  "merchantName": "Coffee Shop",
  "merchantCity": "New York",
  "merchantState": "NY",
  "merchantCountry": "US",
  "cardLastFour": "1234",
  "virtualCardId": "vc-uuid-here",
  "cashbackRate": 0.05
}
```

| Field                                                                 | Type              | Description                                                  |
| --------------------------------------------------------------------- | ----------------- | ------------------------------------------------------------ |
| `eventType`                                                           | String            | 始终为 `TRANSACTION_CREATE`。                                    |
| `recordId`                                                            | String (UUID)     | 唯一的 webhook 事件记录 ID。                                         |
| `transactionId`                                                       | String (UUID)     | 交易 ID。                                                       |
| `accountId`                                                           | String (UUID)     | Fluz 账户 ID。                                                  |
| `userId`                                                              | String (UUID)     | Fluz 用户 ID。                                                  |
| `transactionType`                                                     | String (enum)     | 例如，`PURCHASE`。 *(需确认全集。)*                                    |
| `amount`                                                              | Number            | 以 USD 计的交易金额。                                                |
| `destination`                                                         | String            | 资金去向（如商户名）。                                                  |
| `source`                                                              | String            | 资金来源（例如 `Virtual Card`）。                                     |
| `status`                                                              | String (enum)     | 例如，`COMPLETED`。 *(需确认全集——如 `PENDING`、`SETTLED`、`DECLINED`。)* |
| `channel`                                                             | String (enum)     | 例如，`VIRTUAL_CARD`。 *(需确认全集。)*                                |
| `connectedAppId`                                                      | String            | 事件被路由到的应用。                                                   |
| `connectedAppName`                                                    | String            | 已连接应用的显示名称。                                                  |
| `createdAt`                                                           | String (ISO 8601) | 交易创建时间。                                                      |
| `merchantName` / `merchantCity` / `merchantState` / `merchantCountry` | String            | 商户位置信息。                                                      |
| `cardLastFour`                                                        | String            | 使用卡片的后四位。                                                    |
| `virtualCardId`                                                       | String (UUID)     | 虚拟卡 ID（如适用）。                                                 |
| `cashbackRate`                                                        | Number            | 小数形式的返现比例（例如 `0.05` = 5%）。                                   |

### 交易已更新（`TRANSACTION_UPDATE`）

当交易的状态或详情发生变化时触发——例如，待授权交易结算时。

```json theme={null}
{
  "eventType": "TRANSACTION_UPDATE",
  "recordId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "transactionId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "accountId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
  "userId": "550e8400-e29b-41d4-a716-446655440000",
  "transactionType": "PURCHASE",
  "amount": 42.99,
  "status": "SETTLED",
  "createdAt": "2025-01-15T10:30:00.000Z",
  "updatedAt": "2025-01-16T08:00:00.000Z"
}
```

字段与 `TRANSACTION_CREATE` 对应处一致，并新增 `updatedAt`（ISO 8601）指明变更发生时间。

> 🚧 **一致性检查：** 请确认 `TRANSACTION_UPDATE`（以及 `TRANSACTION_DECLINE`）上均包含 `userId` 与 `connectedAppId`/`connectedAppName`，以便使用方在整个生命周期中一致地识别用户与应用。

### 交易被拒绝（`TRANSACTION_DECLINE`）

当交易被拒绝时触发。包含结构化的拒绝原因，你的应用可以据此采取行动。

```json theme={null}
{
  "eventType": "TRANSACTION_DECLINE",
  "transactionId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "transactionType": "PURCHASE",
  "amount": 500.00,
  "fluzAmount": 500.00,
  "externalFundingAmount": 0,
  "currency": "USD",
  "accountId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
  "userId": "550e8400-e29b-41d4-a716-446655440000",
  "status": "DECLINED",
  "merchantName": "Electronics Store",
  "cardLastFour": "1234",
  "virtualCardId": "vc-uuid-here",
  "isCashBalanceUsed": true,
  "isPrepaymentBalanceUsed": false,
  "isRewardsBalanceUsed": false,
  "isReserveBalanceUsed": false,
  "transactionDateTime": "2025-01-15T10:30:00.000Z",
  "declineTitle": "Transaction Declined",
  "declineReason": "Insufficient funds",
  "declineDescription": "Your account balance was not sufficient for this transaction.",
  "declineCategory": "BALANCE"
}
```

| Field                                                                                             | Type              | Description                                                           |
| ------------------------------------------------------------------------------------------------- | ----------------- | --------------------------------------------------------------------- |
| `amount`                                                                                          | Number            | 尝试的总金额。                                                               |
| `fluzAmount`                                                                                      | Number            | 从 Fluz 余额划拨的部分。                                                       |
| `externalFundingAmount`                                                                           | Number            | 从外部资金划拨的部分。                                                           |
| `currency`                                                                                        | String            | ISO 货币代码（例如 `USD`）。                                                   |
| `status`                                                                                          | String            | `DECLINED`。                                                           |
| `isCashBalanceUsed` / `isPrepaymentBalanceUsed` / `isRewardsBalanceUsed` / `isReserveBalanceUsed` | Boolean           | 尝试从哪些余额类型划拨。                                                          |
| `transactionDateTime`                                                                             | String (ISO 8601) | 拒绝发生的时间。                                                              |
| `declineTitle`                                                                                    | String            | 简短、面向用户的标题。                                                           |
| `declineReason`                                                                                   | String            | 简短原因（例如 `Insufficient funds`）。                                        |
| `declineDescription`                                                                              | String            | 更长的、面向用户的说明。                                                          |
| `declineCategory`                                                                                 | String (enum)     | 例如 `BALANCE`。 *(需确认全集——参见 [Decline Codes](/features/decline-codes)。)* |

### 存款完成（`DEPOSIT_COMPLETE`）

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

```json theme={null}
{
  "eventType": "DEPOSIT_COMPLETE",
  "userId": "550e8400-e29b-41d4-a716-446655440000",
  "accountId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
  "externalReferenceId": "your-reference-123",
  "amount": 50.00
}
```

### 启动 KYC（`WIDGET_KYC_INITIATION`）

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

```json theme={null}
{
  "eventType": "WIDGET_KYC_INITIATION",
  "userId": "550e8400-e29b-41d4-a716-446655440000",
  "accountId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
  "externalReferenceId": "your-reference-123"
}
```

### 转账完成 — 客户到应用（`WIDGET_DEPOSIT_COMPLETE`）

当用户向你的应用转入资金时触发。

```json theme={null}
{
  "eventType": "WIDGET_DEPOSIT_COMPLETE",
  "userId": "550e8400-e29b-41d4-a716-446655440000",
  "accountId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
  "externalReferenceId": "your-reference-123",
  "amount": 25.50
}
```

### 转账完成 — 应用到客户（`WIDGET_WITHDRAW_COMPLETE`）

当你的应用向用户转出资金时触发。

```json theme={null}
{
  "eventType": "WIDGET_WITHDRAW_COMPLETE",
  "userId": "550e8400-e29b-41d4-a716-446655440000",
  "accountId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
  "externalReferenceId": "your-reference-123",
  "amount": 15.00
}
```

### 礼品卡购买（`WIDGET_PURCHASE_GIFT_CARD`）

当通过小部件完成礼品卡购买时触发。

> 🚧 **待完善。** 添加 `WIDGET_PURCHASE_GIFT_CARD` 的负载示例与字段表（例如：商户、金额、礼品卡标识符）。

**常见小部件负载字段：** `userId`（Fluz 用户 ID）、`accountId`（Fluz 账户 ID）、`externalReferenceId`（你在 OAuth 流程中的用户标识符），以及在适用时的 `amount`。

## 最佳实践

* **快速响应，稍后处理。** 立即返回 `200`，并异步处理事件，避免超时与不必要的重试。
* **验证每个请求。** 使用你的 API key 对照原始请求体校验 `X-HMAC-Signature` 后再处理。
* **用事件 ID 去重。** 跟踪 `X-Event-ID` 以处理重试/重复投递。
* **接受未知字段。** 负载可能随时间增加字段；忽略未识别字段而不是失败。
* **监控你的端点。** 针对连续的非 `2xx` 响应发出警报——在 5 次失败后事件会被标记为 `FAILED`。

## 疑难排解

| Symptom        | Likely cause               | Fix                                    |
| -------------- | -------------------------- | -------------------------------------- |
| 未收到任何 webhooks | 端点不可达或返回错误                 | 确认 URL 正确、可通过 HTTPS 公开访问且返回 `2xx`。     |
| 缺少某些事件类型       | 应用缺少所需权限范围                 | 确保你的应用拥有该事件列出的权限范围。                    |
| 公共应用未收到某用户的事件  | 用户未授权所需的权限范围               | 确认用户的 OAuth 授权包含所需的权限范围。               |
| 签名校验失败         | API key 错误，或对重新序列化的请求体进行校验 | 使用你应用的 API key，并基于原始请求体进行验证。           |
| 出现重复投递         | 超时后的重试                     | 使用 `X-Event-ID` 实现幂等性。                 |
| 事件停止到达         | 连续 5 次失败已耗尽重试              | 修复你的端点；新事件会自动恢复投递。联系支持以回放 `FAILED` 事件。 |

## 需要帮助？

* **技术问题：** 检查你的端点日志，并连同 `X-Event-ID` 联系支持。
* **权限范围问题：** 参见 [Application Scopes](/fluz-dashboard/application-scopes) 与 [Decline Codes](/features/decline-codes)。
