Webhook
通过 webhook,你可以在 Fystack 账户中发生特定事件时实时接收通知。
获取 webhook 公钥
获取用于校验工作区 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
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 签名,你需要:
- 规范化 JSON payload(确保对象键的顺序一致)
- 使用你的公钥校验 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,并选择要接收的事件。