---
title: "Webhook"
description: "Fystack webhook 系统的 API 参考。"
url: https://docs.fystack.io/zh/webhooks
last_updated: 2026-11-13
---

> ## Documentation index
> Fetch the complete documentation index at: https://docs.fystack.io/llms.txt
> Use it to discover all available pages before exploring further.

# Webhook

通过 webhook，你可以在 Fystack 账户中发生特定事件时实时接收通知。

---

## 获取 webhook 公钥

**Endpoint:** `GET /workspaces/:workspace_id/webhook-verification-key`

获取用于校验工作区 webhook 签名的公钥。每个 webhook 请求都使用 Ed25519 签名，你应在处理 webhook 数据之前校验该签名。

### 响应

- `success` (boolean): 表示请求是否成功。
- `message` (string): 描述操作结果的消息。
- `code` (integer): 表示结果状态的数字代码（0 表示成功）。
- `data` (object): 包含公钥详情。
  - `public_key` (string): 用于校验 webhook 签名的 Ed25519 公钥。

> **Warning.** 请保存此公钥，校验 webhook 签名时需要用到它。

**Request** `GET /workspaces/:workspace_id/webhook-verification-key`

```typescript
import { FystackSDK, Environment } from "@fystack/sdk"

const apiCredentials = {
  apiKey: process.env.API_KEY,
  apiSecret: process.env.API_SECRET
}

const sdk = new FystackSDK({
  credentials: apiCredentials,
  environment: Environment.Sandbox,
  logger: true
})

const response = await sdk.getWebhookPublicKey(
  "workspace_id"
)
```

```bash
curl -X GET https://api.fystack.io/api/v1/workspaces/workspace_id/webhook-verification-key \
  -H "ACCESS-API-KEY: your_api_key" \
  -H "ACCESS-TIMESTAMP: 1667836889" \
  -H "ACCESS-SIGN: YourBase64EncodedSignature=="
```

```json
{
  "success": true,
  "message": "success",
  "code": 0,
  "data": {
    "public_key": "503d18d8375e60667a4cdb879e72c249b3dc054e7d6443c5ccd93aef7f6547fb"
  }
}
```

---

## Webhook 事件

Fystack 会针对多种事件发送 webhook 通知。每种事件类型都代表账户中的一种特定状态变化。

### 充值事件

- `deposit.pending`: 检测到一笔新的充值，但尚未在区块链上确认。
- `deposit.confirmed`: 一笔充值已在区块链上确认。

> **Warning.** 充值事件的 payload 包含 `idempotency_key` 字段。使用它可以安全地对 webhook 投递进行去重，详见下文的[幂等键](#idempotency-key)。

### 提现事件

- `withdrawal.pending`: 一笔提现请求正在等待审批。
- `withdrawal.executed`: 提现交易已发送到区块链。
- `withdrawal.confirmed`: 提现交易已在区块链上确认。
- `withdrawal.failed`: 提现失败。payload 是 `status: "FAILED"` 的提现对象。嵌套的 `transaction` 对象是可选的，可能为 `null`。

> **Info.** 提现失败时，被冻结的资金会释放回钱包余额。你可以放心地创建新的提现。

---

## Webhook 校验

每个 webhook 请求都会在 `x-webhook-signature` 请求头中包含签名。你应校验该签名，以确保 webhook 来自 Fystack。

### 请求头

- `x-webhook-signature` (string): 规范化 JSON payload 的 Ed25519 签名。
- `x-webhook-event` (string): 触发此 webhook 的事件类型。

### Webhook Payload

- `webhook_id` (string): webhook 配置的 UUID。
- `resource_id` (string): 触发 webhook 的资源的 UUID。
- `url` (string): webhook 发送到的 URL。
- `event` (string): 事件类型（例如 `deposit.confirmed`）。
- `payload` (object): 特定于事件的数据 payload。

### 幂等键

> **Warning.** 对于同一笔充值，充值 webhook（`deposit.pending` 和 `deposit.confirmed`）可能会被投递多次，例如在你的 endpoint 超时或暂时故障后进行重试时。请始终使用 `idempotency_key` 进行去重处理，而不要仅依赖 `resource_id` 或 `id`。

对于充值事件，`payload.idempotency_key` 字段唯一标识一个底层充值事件。由于网络状况或 endpoint 宕机可能导致 Fystack 重新投递同一个 webhook，你应将任何两次具有相同 `idempotency_key` 的投递视为同一事件，并且只处理一次。

- `idempotency_key` (string): 标识该充值事件的唯一键。请保存已处理过的键（例如存入带唯一约束的数据库），如果收到的 webhook 的 `idempotency_key` 已经出现过，则跳过处理。

### 校验签名

要校验 webhook 签名，你需要：

1. 规范化 JSON payload（确保对象键的顺序一致）
2. 使用你的公钥校验 Ed25519 签名

> **Warning.** 校验前务必先规范化事件 payload。不同的 JSON 序列化器可能会为同一数据结构生成不同的字符串表示。

```typescript
import * as ed from '@noble/ed25519'

// Example public key from Fystack
const FYSTACK_PUBLIC_KEY = '503d18d8375e60667a4cdb879e72c249b3dc054e7d6443c5ccd93aef7f6547fb'

// Canonicalize JSON by sorting keys recursively
function canonicalizeJSON(inputObject: Record<string, any>): string {
  if (typeof inputObject !== 'object' || inputObject === null) {
    throw new Error('Input must be a non-null object.')
  }

  const sortKeys = (value: any): any => {
    if (Array.isArray(value)) {
      return value.map(sortKeys)
    }
    if (value && typeof value === 'object' && value.constructor === Object) {
      return Object.keys(value)
        .sort()
        .reduce((sortedObj: Record<string, any>, key: string) => {
          sortedObj[key] = sortKeys(value[key])
          return sortedObj
        }, {})
    }
    return value
  }

  return JSON.stringify(sortKeys(inputObject))
}

// Verify Ed25519 signature
async function verifyWebhook(payload: object, signatureHex: string): Promise<boolean> {
  const canonical = canonicalizeJSON(payload)
  const msg = new TextEncoder().encode(canonical)
  return ed.verify(signatureHex, msg, FYSTACK_PUBLIC_KEY)
}

// Example usage
app.post('/webhook', async (req, res) => {
  const signature = req.headers['x-webhook-signature']
  const event = req.headers['x-webhook-event']
  const payload = req.body

  // Verify signature using your stored public key
  const isValid = await verifyWebhook(
    payload,
    signature
  )

  if (!isValid) {
    return res.status(400).json({ error: 'Invalid signature' })
  }

  // Handle different event types
  switch (event) {
    case 'deposit.confirmed':
      // Handle confirmed deposit
      break
    case 'withdrawal.confirmed':
      // Handle confirmed withdrawal
      break
    // ... handle other events
  }

  res.json({ received: true })
})
```

```go
package main

import (
  "crypto/ed25519"
  "encoding/hex"
  "encoding/json"
  "fmt"
)

// Example public key from Fystack
const FystackPublicKey = "503d18d8375e60667a4cdb879e72c249b3dc054e7d6443c5ccd93aef7f6547fb"

// CanonicalizeJSON ensures consistent JSON representation
func CanonicalizeJSON(data []byte) ([]byte, error) {
  var temp map[string]interface{}
  err := json.Unmarshal(data, &temp)
  if err != nil {
    return nil, fmt.Errorf("invalid JSON: %w", err)
  }

  canonicalJSON, err := json.Marshal(temp)
  if err != nil {
    return nil, fmt.Errorf("failed to marshal JSON: %w", err)
  }
  return canonicalJSON, nil
}

// VerifyWebhook verifies the webhook signature
func VerifyWebhook(payload []byte, signatureHex string) (bool, error) {
  // Decode hex strings
  publicKey, err := hex.DecodeString(FystackPublicKey)
  if err != nil {
    return false, fmt.Errorf("invalid public key: %w", err)
  }

  signature, err := hex.DecodeString(signatureHex)
  if err != nil {
    return false, fmt.Errorf("invalid signature: %w", err)
  }

  // Canonicalize payload
  canonical, err := CanonicalizeJSON(payload)
  if err != nil {
    return false, fmt.Errorf("failed to canonicalize JSON: %w", err)
  }

  // Verify signature
  return ed25519.Verify(publicKey, canonical, signature), nil
}

// Example usage in HTTP handler
func handleWebhook(w http.ResponseWriter, r *http.Request) {
  signature := r.Header.Get("x-webhook-signature")
  event := r.Header.Get("x-webhook-event")

  body, err := io.ReadAll(r.Body)
  if err != nil {
    http.Error(w, "Failed to read body", http.StatusBadRequest)
    return
  }

  isValid, err := VerifyWebhook(
    body,
    signature,
  )
  if err != nil || !isValid {
    http.Error(w, "Invalid signature", http.StatusBadRequest)
    return
  }

  // Handle webhook...
}
```

```php
<?php

// Example public key from Fystack
const FYSTACK_PUBLIC_KEY = '503d18d8375e60667a4cdb879e72c249b3dc054e7d6443c5ccd93aef7f6547fb';

/**
 * Canonicalize JSON by decoding and re-encoding with sorted keys
 */
function canonicalizeJSON($data) {
    $decoded = json_decode($data, true);
    if (json_last_error() !== JSON_ERROR_NONE) {
        throw new Exception('Invalid JSON: ' . json_last_error_msg());
    }

    // Sort all arrays and objects recursively
    array_walk_recursive($decoded, function(&$item, $key) {
        if (is_array($item)) {
            ksort($item);
        }
    });
    ksort($decoded);

    return json_encode($decoded);
}

/**
 * Verify webhook signature using Ed25519
 */
function verifyWebhook($payload, $signatureHex) {
    try {
        // Convert hex to binary
        $publicKey = hex2bin(FYSTACK_PUBLIC_KEY);
        $signature = hex2bin($signatureHex);

        // Canonicalize payload
        $canonical = canonicalizeJSON($payload);

        // Verify using sodium
        return sodium_crypto_sign_verify_detached(
            $signature,
            $canonical,
            $publicKey
        );
    } catch (Exception $e) {
        error_log('Webhook verification failed: ' . $e->getMessage());
        return false;
    }
}

// Example usage in a webhook handler
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$event = $_SERVER['HTTP_X_WEBHOOK_EVENT'] ?? '';
$payload = file_get_contents('php://input');

if (!verifyWebhook(
    $payload,
    $signature
)) {
    http_response_code(400);
    echo json_encode(['error' => 'Invalid signature']);
    exit;
}

// Handle different event types
switch ($event) {
    case 'deposit.confirmed':
        // Handle confirmed deposit
        break;
    case 'withdrawal.confirmed':
        // Handle confirmed withdrawal
        break;
}

echo json_encode(['received' => true]);
```

```python
import json
import ed25519
from typing import Dict, Any
from binascii import unhexlify

# Example public key from Fystack
FYSTACK_PUBLIC_KEY = '503d18d8375e60667a4cdb879e72c249b3dc054e7d6443c5ccd93aef7f6547fb'

def canonicalize_json(data: Dict[Any, Any]) -> str:
    """Ensure consistent JSON representation."""
    return json.dumps(data, sort_keys=True, separators=(',', ':'))

def verify_webhook(payload: Dict[Any, Any], signature_hex: str) -> bool:
    """Verify webhook signature."""
    try:
        # Convert hex strings to bytes
        public_key = unhexlify(FYSTACK_PUBLIC_KEY)
        signature = unhexlify(signature_hex)

        # Canonicalize payload
        canonical = canonicalize_json(payload).encode('utf-8')

        # Create verifier
        verifier = ed25519.VerifyingKey(public_key)
        return verifier.verify(signature, canonical)
    except Exception as e:
        print(f"Verification failed: {e}")
        return False

# Example Flask route
@app.route('/webhook', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('x-webhook-signature')
    event = request.headers.get('x-webhook-event')
    payload = request.json

    if not verify_webhook(
        payload,
        signature
    ):
        return jsonify({'error': 'Invalid signature'}), 400

    # Handle webhook...
    return jsonify({'received': True})
```

```java
import com.fasterxml.jackson.databind.ObjectMapper;
import org.bouncycastle.crypto.signers.Ed25519Signer;
import org.bouncycastle.util.encoders.Hex;

public class WebhookVerifier {
    private static final ObjectMapper MAPPER = new ObjectMapper();

    // Example public key from Fystack
    private static final String FYSTACK_PUBLIC_KEY = "503d18d8375e60667a4cdb879e72c249b3dc054e7d6443c5ccd93aef7f6547fb";

    public static String canonicalizeJSON(Object data) throws JsonProcessingException {
        // Use Jackson's sorted map feature for consistent ordering
        return MAPPER.writer().writeValueAsString(data);
    }

    public static boolean verifyWebhook(Object payload, String signatureHex) {
        try {
            byte[] publicKey = Hex.decode(FYSTACK_PUBLIC_KEY);
            byte[] signature = Hex.decode(signatureHex);

            // Canonicalize payload
            String canonical = canonicalizeJSON(payload);
            byte[] message = canonical.getBytes(StandardCharsets.UTF_8);

            Ed25519Signer verifier = new Ed25519Signer();
            verifier.init(false, new Ed25519PublicKeyParameters(publicKey, 0));
            verifier.update(message, 0, message.length);

            return verifier.verifySignature(signature);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    // Example Spring controller
    @PostMapping("/webhook")
    public ResponseEntity<?> handleWebhook(
        @RequestHeader("x-webhook-signature") String signature,
        @RequestHeader("x-webhook-event") String event,
        @RequestBody Map<String, Object> payload
    ) {
        if (!verifyWebhook(
            payload,
            signature
        )) {
            return ResponseEntity.badRequest().body("Invalid signature");
        }

        // Handle webhook...
        return ResponseEntity.ok().build();
    }
}
```

> **Info.** 规范化步骤至关重要：它确保相同的 JSON 数据无论最初如何格式化或排序，都始终生成相同的字符串表示。

### 事件 Payload 示例

**Event Examples**

```json
{
  "webhook_id": "73893532-8e3d-4bc7-b3f9-148c9b82b811",
  "resource_id": "62ef8383-e897-449f-b9d8-78fffaa26a61",
  "url": "https://webhook.site/56e92433-823d-4b28-9cc9-9298076af671",
  "payload": {
    "amount": "1000",
    "asset": {
      "address": "0xDD75BEb521f4513D65fFbbE91ab7131f3cD92080",
      "address_type": "evm",
      "created_at": "2024-04-12T22:58:36.133934+07:00",
      "decimals": 6,
      "id": "f1be472d-1338-43ff-b547-06faba7ecce7",
      "is_native": false,
      "is_whitelisted": true,
      "logo_url": "https://assets-cdn.trustwallet.com/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png",
      "name": "USDC Sepolia test",
      "network_id": "23d13c03-1f3c-4e2a-9aef-1771a244b7f6",
      "symbol": "USDC",
      "updated_at": "2024-04-12T22:58:36.133934+07:00"
    },
    "asset_hold": null,
    "asset_hold_id": null,
    "asset_id": "f1be472d-1338-43ff-b547-06faba7ecce7",
    "block_number": 8960514,
    "created_at": "2025-08-11T17:31:24+07:00",
    "description": "",
    "direction": "in",
    "from_address": "0xe6EBF81E9C225BbCEa9b5399E0D0d0f29f30f119",
    "id": "62ef8383-e897-449f-b9d8-78fffaa26a61",
    "idempotency_key": "bfa74b5d5b591794031e0f78a556c80b7b89875378baac4df75b8302cf80b72c",
    "method": "transfer",
    "network": null,
    "network_id": "23d13c03-1f3c-4e2a-9aef-1771a244b7f6",
    "price_native_token": "4260.480000022453",
    "price_token": "0.9997516371944777",
    "status": "pending",
    "to_address": "0xA118fa3d039BA6ad3BB0bA8ac8F243F727B2F99F",
    "tx_fee": "0.000051521446618",
    "tx_hash": "0x21c201ff8e02abdcd4395c71639266d953cb18cb159bdcd28b7556c482b4881a",
    "type": "token_transfer",
    "updated_at": "2025-08-11T17:31:26.815540431+07:00",
    "user": null,
    "user_id": "075243c1-962e-4803-83da-d73c78b8b060",
    "wallet": {
      "created_at": "2025-08-11T10:35:54.174577+07:00",
      "deleted_at": null,
      "disabled": false,
      "id": "e0851607-417a-475a-b399-5f3aa262b81d",
      "name": "hello1",
      "threshold": 1,
      "updated_at": "2025-08-11T10:35:54.174577+07:00",
      "user_id": "075243c1-962e-4803-83da-d73c78b8b060",
      "wallet_purpose": "general",
      "wallet_type": "standard",
      "workspace_id": "6bcfe6c7-281b-4773-a188-e78f5cad3336"
    },
    "wallet_id": "e0851607-417a-475a-b399-5f3aa262b81d",
    "workspace": null,
    "workspace_id": "6bcfe6c7-281b-4773-a188-e78f5cad3336"
  },
  "event": "deposit.pending"
}
```

```json
{
  "webhook_id": "73893532-8e3d-4bc7-b3f9-148c9b82b811",
  "resource_id": "c6702df0-b8ef-4a38-882d-dabf0d56127d",
  "url": "https://webhook.site/56e92433-823d-4b28-9cc9-9298076af671",
  "event": "deposit.confirmed",
  "payload": {
    "id": "c6702df0-b8ef-4a38-882d-dabf0d56127d",
    "idempotency_key": "a1c92e7f4d3b8619524fbd0b3e9c15a6ed172b8d31f645a998e2c07b1a5f6d3e",
    "amount": "1000",
    "status": "confirmed",
    "tx_hash": "0x75789cbbe4d55328e6be7b7155b4028ea8afa384ffe13a1146d8bdb3eed6ba22",
    "from_address": "0xe6EBF81E9C225BbCEa9b5399E0D0d0f29f30f119",
    "to_address": "0xA793457d84CFD255Ed68295984300C4f799E759F",
    "asset_id": "f1be472d-1338-43ff-b547-06faba7ecce7",
    "wallet_id": "e0851607-417a-475a-b399-5f3aa262b81d",
    "block_number": 8960538,
    "type": "token_transfer",
    "direction": "in"
  }
}
```

```json
{
  "webhook_id": "73893532-8e3d-4bc7-b3f9-148c9b82b811",
  "resource_id": "147d4808-2e98-4a86-9ead-c227e7e80cf6",
  "url": "https://webhook.site/56e92433-823d-4b28-9cc9-9298076af671",
  "event": "withdrawal.pending",
  "payload": {
    "id": "0de79230-a820-4bd4-b020-883d395faa87",
    "amount": "1000",
    "status": "PENDING_APPROVAL",
    "recipient_address": "0x7c2BbE2e02af09F1cf9aFd4A79EC201DC841675a",
    "asset_id": "f1be472d-1338-43ff-b547-06faba7ecce7",
    "wallet_id": "e0851607-417a-475a-b399-5f3aa262b81d",
    "creator_id": "075243c1-962e-4803-83da-d73c78b8b060",
    "asset_hold_id": "2595ac0b-deb6-4c58-8466-290a248e5d2f",
    "created_at": "2025-08-11T17:37:29.815148+07:00",
    "updated_at": "2025-08-11T17:37:29.815148+07:00"
  }
}
```

```json
{
  "webhook_id": "73893532-8e3d-4bc7-b3f9-148c9b82b811",
  "resource_id": "0de79230-a820-4bd4-b020-883d395faa87",
  "url": "https://webhook.site/56e92433-823d-4b28-9cc9-9298076af671",
  "payload": {
    "amount": "1000",
    "asset": null,
    "asset_hold_id": "2595ac0b-deb6-4c58-8466-290a248e5d2f",
    "asset_id": "f1be472d-1338-43ff-b547-06faba7ecce7",
    "created_at": "2025-08-11T17:37:29.815148+07:00",
    "creator": null,
    "creator_id": "075243c1-962e-4803-83da-d73c78b8b060",
    "error_reason": null,
    "id": "0de79230-a820-4bd4-b020-883d395faa87",
    "recipient_address": "0x7c2BbE2e02af09F1cf9aFd4A79EC201DC841675a",
    "status": "EXECUTED",
    "transaction": {
      "amount": "1000",
      "asset": null,
      "asset_hold": null,
      "asset_hold_id": "2595ac0b-deb6-4c58-8466-290a248e5d2f",
      "asset_id": "f1be472d-1338-43ff-b547-06faba7ecce7",
      "block_number": 8960546,
      "created_at": "2025-08-11T17:37:45.828992+07:00",
      "description": "",
      "direction": "out",
      "from_address": "0xA118fa3d039BA6ad3BB0bA8ac8F243F727B2F99F",
      "id": "147d4808-2e98-4a86-9ead-c227e7e80cf6",
      "method": "transfer",
      "network": null,
      "network_id": "23d13c03-1f3c-4e2a-9aef-1771a244b7f6",
      "price_native_token": "4260.480000022453",
      "price_token": "0.9997516371944777",
      "status": "completed",
      "to_address": "0x7c2BbE2e02af09F1cf9aFd4A79EC201DC841675a",
      "tx_fee": "0.0000001851509817",
      "tx_hash": "0x619dea0c5eb5206a35ea22113d6b4d9e5f0ce226ae3b2ca0089ec9c341b3a210",
      "type": "withdrawal",
      "updated_at": "2025-08-11T17:37:50.765281+07:00",
      "user": null,
      "user_id": "075243c1-962e-4803-83da-d73c78b8b060",
      "wallet": null,
      "wallet_id": "e0851607-417a-475a-b399-5f3aa262b81d",
      "workspace": null,
      "workspace_id": "6bcfe6c7-281b-4773-a188-e78f5cad3336"
    },
    "transaction_id": "147d4808-2e98-4a86-9ead-c227e7e80cf6",
    "updated_at": "2025-08-11T17:37:46.069818+07:00",
    "wallet_id": "e0851607-417a-475a-b399-5f3aa262b81d",
    "withdrawal_approvals": null
  },
  "event": "withdrawal.executed"
}
```

```json
{
  "webhook_id": "73893532-8e3d-4bc7-b3f9-148c9b82b811",
  "resource_id": "147d4808-2e98-4a86-9ead-c227e7e80cf6",
  "url": "https://webhook.site/56e92433-823d-4b28-9cc9-9298076af671",
  "payload": {
    "amount": "1000",
    "asset": null,
    "asset_hold_id": "2595ac0b-deb6-4c58-8466-290a248e5d2f",
    "asset_id": "f1be472d-1338-43ff-b547-06faba7ecce7",
    "created_at": "2025-08-11T17:37:29.815148+07:00",
    "creator": null,
    "creator_id": "075243c1-962e-4803-83da-d73c78b8b060",
    "error_reason": null,
    "id": "0de79230-a820-4bd4-b020-883d395faa87",
    "recipient_address": "0x7c2BbE2e02af09F1cf9aFd4A79EC201DC841675a",
    "status": "SUCCESS",
    "transaction": {
      "amount": "1000",
      "asset": null,
      "asset_hold": null,
      "asset_hold_id": "2595ac0b-deb6-4c58-8466-290a248e5d2f",
      "asset_id": "f1be472d-1338-43ff-b547-06faba7ecce7",
      "block_number": 8960546,
      "created_at": "2025-08-11T17:37:45.828992+07:00",
      "description": "",
      "direction": "out",
      "from_address": "0xA118fa3d039BA6ad3BB0bA8ac8F243F727B2F99F",
      "id": "147d4808-2e98-4a86-9ead-c227e7e80cf6",
      "method": "transfer",
      "network": null,
      "network_id": "23d13c03-1f3c-4e2a-9aef-1771a244b7f6",
      "price_native_token": "4260.480000022453",
      "price_token": "0.9997516371944777",
      "status": "confirmed",
      "to_address": "0x7c2BbE2e02af09F1cf9aFd4A79EC201DC841675a",
      "tx_fee": "0.0000001851509817",
      "tx_hash": "0x619dea0c5eb5206a35ea22113d6b4d9e5f0ce226ae3b2ca0089ec9c341b3a210",
      "type": "withdrawal",
      "updated_at": "2025-08-11T17:38:05.607714+07:00",
      "user": null,
      "user_id": "075243c1-962e-4803-83da-d73c78b8b060",
      "wallet": null,
      "wallet_id": "e0851607-417a-475a-b399-5f3aa262b81d",
      "workspace": null,
      "workspace_id": "6bcfe6c7-281b-4773-a188-e78f5cad3336"
    },
    "transaction_id": "147d4808-2e98-4a86-9ead-c227e7e80cf6",
    "updated_at": "2025-08-11T17:38:06.057452+07:00",
    "wallet_id": "e0851607-417a-475a-b399-5f3aa262b81d",
    "withdrawal_approvals": null
  },
  "event": "withdrawal.confirmed"
}
```

```json
{
  "webhook_id": "73893532-8e3d-4bc7-b3f9-148c9b82b811",
  "resource_id": "3e8a6d21-7f4c-4b19-a0d5-2c9e1b7f8a36",
  "url": "https://webhook.site/56e92433-823d-4b28-9cc9-9298076af671",
  "payload": {
    "id": "3e8a6d21-7f4c-4b19-a0d5-2c9e1b7f8a36",
    "amount": "1000",
    "status": "FAILED",
    "recipient_address": "0x7c2BbE2e02af09F1cf9aFd4A79EC201DC841675a",
    "asset": null,
    "asset_id": "f1be472d-1338-43ff-b547-06faba7ecce7",
    "asset_hold_id": "8f4b2a90-1c6d-4e57-b3a8-0d9e6f1c2b75",
    "wallet_id": "e0851607-417a-475a-b399-5f3aa262b81d",
    "creator": null,
    "creator_id": "075243c1-962e-4803-83da-d73c78b8b060",
    "error_reason": null,
    "fee_priority": "standard",
    "approval_threshold": 0,
    "withdrawal_approvals": null,
    "transaction_id": "d47c1e93-5a2b-4f80-9e6d-1b3a8c7f0e52",
    "transaction": {
      "amount": "1000",
      "asset": null,
      "asset_hold": null,
      "asset_hold_id": "8f4b2a90-1c6d-4e57-b3a8-0d9e6f1c2b75",
      "asset_id": "f1be472d-1338-43ff-b547-06faba7ecce7",
      "block_number": 0,
      "created_at": "2025-08-11T18:10:02.118254+07:00",
      "description": "",
      "direction": "out",
      "from_address": "0xA118fa3d039BA6ad3BB0bA8ac8F243F727B2F99F",
      "id": "d47c1e93-5a2b-4f80-9e6d-1b3a8c7f0e52",
      "method": "transfer",
      "network": null,
      "network_id": "23d13c03-1f3c-4e2a-9aef-1771a244b7f6",
      "failed_reason": "Signing timeout",
      "price_native_token": "0",
      "price_token": "0",
      "status": "failed",
      "to_address": "0x7c2BbE2e02af09F1cf9aFd4A79EC201DC841675a",
      "tx_fee": "0",
      "tx_hash": null,
      "type": "withdrawal",
      "updated_at": "2025-08-11T18:15:02.540917+07:00",
      "user": null,
      "user_id": "075243c1-962e-4803-83da-d73c78b8b060",
      "wallet": null,
      "wallet_id": "e0851607-417a-475a-b399-5f3aa262b81d",
      "workspace": null,
      "workspace_id": "6bcfe6c7-281b-4773-a188-e78f5cad3336"
    },
    "created_at": "2025-08-11T18:09:58.774102+07:00",
    "updated_at": "2025-08-11T18:15:02.611385+07:00"
  },
  "event": "withdrawal.failed"
}
```

> **Info.** 请在 Fystack 控制台中注册你的 webhook endpoint，并选择要接收的事件。
