Webhook

Webhook cho phép bạn nhận thông báo theo thời gian thực khi các sự kiện cụ thể xảy ra trong tài khoản Fystack của bạn.


Lấy khóa công khai của webhook

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

Truy xuất khóa công khai dùng để xác minh chữ ký webhook cho workspace của bạn. Mỗi yêu cầu webhook đều được ký bằng Ed25519, và bạn nên xác minh chữ ký này trước khi xử lý dữ liệu webhook.

Phản hồi

  • Name
    success
    Type
    boolean
    Description

    Cho biết yêu cầu có thành công hay không.

  • Name
    message
    Type
    string
    Description

    Thông báo mô tả kết quả của thao tác.

  • Name
    code
    Type
    integer
    Description

    Mã số cho biết trạng thái kết quả (0 nghĩa là thành công).

  • Name
    data
    Type
    object
    Description

    Chứa thông tin chi tiết của khóa công khai.

  • Name
    public_key
    Type
    string
    Description

    Khóa công khai Ed25519 dùng để xác minh chữ ký webhook.

Hãy lưu khóa công khai này, bạn sẽ cần nó để xác minh chữ ký 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"
  }
}

Sự kiện webhook

Fystack gửi thông báo webhook cho nhiều sự kiện khác nhau. Mỗi loại sự kiện đại diện cho một thay đổi trạng thái cụ thể trong tài khoản của bạn.

Sự kiện nạp tiền

  • Name
    deposit.pending
    Type
    Description

    Một giao dịch nạp tiền mới đã được phát hiện nhưng chưa được xác nhận trên blockchain.

  • Name
    deposit.confirmed
    Type
    Description

    Một giao dịch nạp tiền đã được xác nhận trên blockchain.

Payload của sự kiện nạp tiền bao gồm trường idempotency_key. Hãy sử dụng nó để loại bỏ trùng lặp các lần gửi webhook một cách an toàn, xem Idempotency Key bên dưới.

Sự kiện rút tiền

  • Name
    withdrawal.pending
    Type
    Description

    Một yêu cầu rút tiền đang chờ phê duyệt.

  • Name
    withdrawal.executed
    Type
    Description

    Giao dịch rút tiền đã được gửi lên blockchain.

  • Name
    withdrawal.confirmed
    Type
    Description

    Giao dịch rút tiền đã được xác nhận trên blockchain.

  • Name
    withdrawal.failed
    Type
    Description

    Giao dịch rút tiền đã thất bại. Payload là đối tượng withdrawal với status: "FAILED". Đối tượng transaction lồng bên trong là tùy chọn và có thể là null.

Khi một giao dịch rút tiền thất bại, số tiền bị tạm giữ sẽ được hoàn lại vào số dư ví. Bạn có thể tạo giao dịch rút tiền mới một cách an toàn.


Xác minh webhook

Mỗi yêu cầu webhook đều bao gồm một chữ ký trong header x-webhook-signature. Bạn nên xác minh chữ ký này để đảm bảo webhook đến từ Fystack.

Header

  • Name
    x-webhook-signature
    Type
    string
    Description

    Chữ ký Ed25519 của payload JSON đã được chuẩn hóa.

  • Name
    x-webhook-event
    Type
    string
    Description

    Loại sự kiện đã kích hoạt webhook này.

Payload của webhook

  • Name
    webhook_id
    Type
    string
    Description

    UUID của cấu hình webhook.

  • Name
    resource_id
    Type
    string
    Description

    UUID của tài nguyên đã kích hoạt webhook.

  • Name
    url
    Type
    string
    Description

    URL mà webhook đã được gửi đến.

  • Name
    event
    Type
    string
    Description

    Loại sự kiện (ví dụ: deposit.confirmed).

  • Name
    payload
    Type
    object
    Description

    Payload dữ liệu riêng của sự kiện.

Idempotency Key

Webhook nạp tiền (deposit.pending và deposit.confirmed) có thể được gửi nhiều lần cho cùng một giao dịch nạp tiền, ví dụ sau khi thử lại do hết thời gian chờ hoặc do endpoint của bạn tạm thời gặp sự cố. Luôn sử dụng idempotency_key để loại bỏ xử lý trùng lặp thay vì chỉ dựa vào resource_id hoặc id.

Đối với sự kiện nạp tiền, trường payload.idempotency_key định danh duy nhất một sự kiện nạp tiền gốc. Vì điều kiện mạng hoặc thời gian ngừng hoạt động của endpoint có thể khiến Fystack gửi lại cùng một webhook, bạn nên coi hai lần gửi bất kỳ có cùng idempotency_key là cùng một sự kiện và chỉ xử lý một lần.

  • Name
    idempotency_key
    Type
    string
    Description

    Khóa duy nhất định danh sự kiện nạp tiền này. Hãy lưu các khóa bạn đã xử lý (ví dụ: trong cơ sở dữ liệu có ràng buộc unique) và bỏ qua việc xử lý nếu idempotency_key của webhook đến đã từng xuất hiện.

Xác minh chữ ký

Để xác minh chữ ký webhook, bạn cần:

  1. Chuẩn hóa payload JSON (đảm bảo thứ tự các khóa của đối tượng nhất quán)
  2. Xác minh chữ ký Ed25519 bằng khóa công khai của bạn

Luôn chuẩn hóa payload sự kiện trước khi xác minh. Các bộ tuần tự hóa JSON khác nhau có thể tạo ra các chuỗi biểu diễn khác nhau cho cùng một cấu trúc dữ liệu.

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

Bước chuẩn hóa là rất quan trọng: nó đảm bảo cùng một dữ liệu JSON sẽ luôn tạo ra cùng một chuỗi biểu diễn, bất kể ban đầu nó được định dạng hay sắp xếp như thế nào.

Ví dụ payload sự kiện

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

Đăng ký các endpoint webhook của bạn trên cổng Fystack và chọn những sự kiện bạn muốn nhận.