Webhook

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


获取 webhook 公钥

GET
/workspaces/{workspace_id}/webhook-verification-key

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

响应

  • Name
    success
    Type
    boolean
    Description

    表示请求是否成功。

  • Name
    message
    Type
    string
    Description

    描述操作结果的消息。

  • Name
    code
    Type
    integer
    Description

    表示结果状态的数字代码(0 表示成功)。

  • Name
    data
    Type
    object
    Description

    包含公钥详情。

  • Name
    public_key
    Type
    string
    Description

    用于校验 webhook 签名的 Ed25519 公钥。

请保存此公钥,校验 webhook 签名时需要用到它。

Request

GET
/workspaces/:workspace_id/webhook-verification-key
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"
)

Response

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

Webhook 事件

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

充值事件

  • Name
    deposit.pending
    Type
    Description

    检测到一笔新的充值,但尚未在区块链上确认。

  • Name
    deposit.confirmed
    Type
    Description

    一笔充值已在区块链上确认。

充值事件的 payload 包含 idempotency_key 字段。使用它可以安全地对 webhook 投递进行去重,详见下文的幂等键。

提现事件

  • Name
    withdrawal.pending
    Type
    Description

    一笔提现请求正在等待审批。

  • Name
    withdrawal.executed
    Type
    Description

    提现交易已发送到区块链。

  • Name
    withdrawal.confirmed
    Type
    Description

    提现交易已在区块链上确认。

  • Name
    withdrawal.failed
    Type
    Description

    提现失败。payload 是 status: "FAILED" 的提现对象。嵌套的 transaction 对象是可选的,可能为 null。

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


Webhook 校验

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

请求头

  • Name
    x-webhook-signature
    Type
    string
    Description

    规范化 JSON payload 的 Ed25519 签名。

  • Name
    x-webhook-event
    Type
    string
    Description

    触发此 webhook 的事件类型。

Webhook Payload

  • Name
    webhook_id
    Type
    string
    Description

    webhook 配置的 UUID。

  • Name
    resource_id
    Type
    string
    Description

    触发 webhook 的资源的 UUID。

  • Name
    url
    Type
    string
    Description

    webhook 发送到的 URL。

  • Name
    event
    Type
    string
    Description

    事件类型(例如 deposit.confirmed)。

  • Name
    payload
    Type
    object
    Description

    特定于事件的数据 payload。

幂等键

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

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

  • Name
    idempotency_key
    Type
    string
    Description

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

校验签名

要校验 webhook 签名,你需要:

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

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

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 })
})

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

事件 Payload 示例

Event Examples

{
  "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"
}

请在 Fystack 控制台中注册你的 webhook endpoint,并选择要接收的事件。