Webhooks

Webhooks allow you to receive real-time notifications when specific events occur in your Fystack account.


GET/workspaces/:workspace_id/webhook-verification-key

Get webhook public key

Retrieve the public key used to verify webhook signatures for your workspace. Each webhook request is signed using Ed25519, and you should verify this signature before processing the webhook data.

Response

  • Name
    success
    Type
    boolean
    Description

    Indicates whether the request was successful.

  • Name
    message
    Type
    string
    Description

    A message describing the result of the operation.

  • Name
    code
    Type
    integer
    Description

    A numeric code indicating the result status (0 for success).

  • Name
    data
    Type
    object
    Description

    Contains the public key details.

  • Name
    public_key
    Type
    string
    Description

    The Ed25519 public key for webhook signature verification.

Store this public key, you'll need it to verify webhook signatures.

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

Webhook Events

Fystack sends webhook notifications for various events. Each event type represents a specific state change in your account.

Deposit Events

  • Name
    deposit.pending
    Type
    Description

    A new deposit has been detected but not yet confirmed on the blockchain.

  • Name
    deposit.confirmed
    Type
    Description

    A deposit has been confirmed on the blockchain.

Withdrawal Events

  • Name
    withdrawal.pending
    Type
    Description

    A withdrawal request is pending approval.

  • Name
    withdrawal.executed
    Type
    Description

    The withdrawal transaction has been sent to the blockchain.

  • Name
    withdrawal.confirmed
    Type
    Description

    The withdrawal transaction has been confirmed on the blockchain.

  • Name
    withdrawal.failed
    Type
    Description

    The withdrawal transaction has failed.


Webhook Verification

Each webhook request includes a signature in the x-webhook-signature header. You should verify this signature to ensure the webhook came from Fystack.

Headers

  • Name
    x-webhook-signature
    Type
    string
    Description

    Ed25519 signature of the canonicalized JSON payload.

  • Name
    x-webhook-event
    Type
    string
    Description

    The type of event that triggered this webhook.

Webhook Payload

  • Name
    webhook_id
    Type
    string
    Description

    The UUID of the webhook configuration.

  • Name
    resource_id
    Type
    string
    Description

    The UUID of the resource that triggered the webhook.

  • Name
    url
    Type
    string
    Description

    The URL where the webhook was sent.

  • Name
    event
    Type
    string
    Description

    The type of event (e.g., deposit.confirmed).

  • Name
    payload
    Type
    object
    Description

    The event-specific data payload.

Verifying Signatures

To verify webhook signatures, you'll need to:

  1. Canonicalize the JSON payload (ensure consistent ordering of object keys)
  2. Verify the Ed25519 signature using your public key

Always canonicalize the event payload before verification. Different JSON serializers might produce different string representations of the same data structure.

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

The canonicalization step is crucial - it ensures that the same JSON data will always produce the same string representation, regardless of how it was originally formatted or ordered.

Example Event Payloads

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",
    "method": "transfer",
    "network": null,
    "network_id": "23d13c03-1f3c-4e2a-9aef-1771a244b7f6",
    "omitempty": null,
    "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"
}

Would you like me to add examples for other withdrawal events like withdrawal.executed, withdrawal.confirmed, and withdrawal.failed as well?

Register your webhook endpoints in the Fystack portal and select which events you want to receive.