> ## 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（Webhook）

Webhook 讓你的應用程式在 Fluz 平台上發生事件時即時接收通知。你不需要輪詢變更，而是註冊一個 HTTPS 端點，Fluz 會在事件發生的當下把事件資料推送給你——例如虛擬卡交易、被拒絕的購買、完成的入金，等等。

<Note>
  本頁作為 Webhooks 的頂層說明，因為 webhook 事件跨越多個平台區域（元件流程與交易活動），而不是屬於單一功能。
</Note>

Webhooks 適用於 Fluz 平台上的所有應用程式類型：在你自有帳戶上運作的私人應用、公用 OAuth 應用（透過 API 代表其他使用者操作），以及嵌入式元件應用。

## 運作方式

1. 在開發者入口註冊你的應用程式的 webhook URL。
2. 選擇你要接收的事件類型（或訂閱全部）。
3. 當符合的事件發生時，Fluz 會以帶簽章的 JSON 有效負載發出 HTTP `POST` 至你的 URL。
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 狀態更新。

### 元件應用（Widget apps）

元件應用是特殊的公用應用。事件以相同方式路由（基於 OAuth 關係），且元件應用另外支援元件專屬事件，例如轉帳完成與禮品卡購買。

## 事件類型

只有在你的應用——以及對於公用/OAuth 應用，個別使用者的 OAuth 授權——持有所需的範圍（scopes）時，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. 選取事件

選擇你想接收的事件類型。**將選取留空可作為接收全部**，你會收到你的應用具備 scope 的所有事件。

### 5. 儲存

點擊 **Create Webhook**。你的端點會立即開始接收事件。

## 管理 webhooks

* **多個端點** — 你可以在每個應用程式註冊多個 webhook URL。
* **變更訂閱事件** — 刪除該 webhook，並以新的事件選擇重新建立。
* **移除 webhook** — 點擊其旁的 **Remove**。該 webhook 會立即封存並停止接收事件。

## 接收 webhooks

### 請求格式

每個 webhook 都以 HTTP `POST` 傳遞，並包含以下標頭：

| Header             | Description                                |
| ------------------ | ------------------------------------------ |
| `Content-Type`     | 一律為 `application/json`。                    |
| `X-HMAC-Signature` | 以你的應用 API 金鑰簽署的原始 JSON 本文的 HMAC-SHA256 簽章。 |
| `X-Event-ID`       | 此事件的唯一 UUID — 用於去重。                        |

本文為 JSON 物件，且每個有效負載都包含 `eventType` 欄位來識別事件。

### 端點需求

* **僅限 HTTPS** — 純 HTTP 端點會在註冊時被拒。
* **可公開存取**，且可接受 `POST` 請求。
* **在 30 秒內以 `2xx` 回應。** 非 `2xx` 回應或逾時會觸發重試。
* **驗證每個請求的 HMAC 簽章**。

## 驗證簽章

每次傳遞都包含 `X-HMAC-Signature` 標頭——以你應用的 **API 金鑰** 對「原始」JSON 本文計算的 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` 標頭進行「去重」。在生產環境中使用 Redis 或資料庫持久化已處理的 ID，跳過已處理的事件。
* 以資料排序，而非到達順序。若順序重要，請依據有效負載上的時間戳（`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 金鑰，針對原始本文驗證 `X-HMAC-Signature` 後再處理。
* **以事件 ID 去重。** 追蹤 `X-Event-ID` 以處理重試/重複投遞。
* **接受未知欄位。** 有效負載可能隨時間新增欄位；忽略未識別欄位而非失敗。
* **監控你的端點。** 對連續的非 `2xx` 回應發出警報——在 5 次失敗後事件會被標記為 `FAILED`。

## 疑難排解

| Symptom         | Likely cause            | Fix                                  |
| --------------- | ----------------------- | ------------------------------------ |
| 沒有收到任何 webhooks | 端點無法連線或回傳錯誤             | 確認 URL 正確、可透過 HTTPS 公開存取，且回傳 `2xx`。  |
| 少了某些事件類型        | 應用缺少必要的 scopes          | 確保你的應用持有所列事件所需的 scopes。              |
| 公用應用未收到某使用者的事件  | 使用者未授與必要的 scopes        | 確認該使用者的 OAuth 授權包含所需 scopes。         |
| 簽章驗證失敗          | 錯誤的 API 金鑰，或對重新序列化的本文驗證 | 使用你的應用 API 金鑰，並針對原始請求本文驗證。           |
| 收到重複投遞          | 逾時後的重試                  | 以 `X-Event-ID` 實作冪等性。                |
| 事件停止傳送          | 連續 5 次失敗已耗盡重試           | 修復你的端點；新事件會自動恢復。聯絡客服以重播 `FAILED` 事件。 |

## 需要協助？

* **技術問題：** 請檢查你的端點日誌，並提供 `X-Event-ID` 與客服聯繫。
* **Scope 相關問題：** 參見 [Application Scopes](/fluz-dashboard/application-scopes) 與 [Decline Codes](/features/decline-codes)。
