Webhooks

Los webhooks te permiten recibir notificaciones en tiempo real cuando ocurren eventos específicos en tu cuenta de Fystack.


Obtener la clave pública de webhook

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

Obtén la clave pública que se usa para verificar las firmas de webhook de tu espacio de trabajo. Cada solicitud de webhook se firma con Ed25519, y debes verificar esta firma antes de procesar los datos del webhook.

Respuesta

  • Name
    success
    Type
    boolean
    Description

    Indica si la solicitud se realizó correctamente.

  • Name
    message
    Type
    string
    Description

    Un mensaje que describe el resultado de la operación.

  • Name
    code
    Type
    integer
    Description

    Un código numérico que indica el estado del resultado (0 si es exitoso).

  • Name
    data
    Type
    object
    Description

    Contiene los detalles de la clave pública.

  • Name
    public_key
    Type
    string
    Description

    La clave pública Ed25519 para verificar las firmas de webhook.

Guarda esta clave pública, la necesitarás para verificar las firmas de 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"
  }
}

Eventos de webhook

Fystack envía notificaciones de webhook para varios eventos. Cada tipo de evento representa un cambio de estado específico en tu cuenta.

Eventos de depósito

  • Name
    deposit.pending
    Type
    Description

    Se detectó un nuevo depósito, pero aún no se ha confirmado en la blockchain.

  • Name
    deposit.confirmed
    Type
    Description

    Un depósito se confirmó en la blockchain.

Los payloads de los eventos de depósito incluyen un campo idempotency_key. Úsalo para deduplicar de forma segura las entregas de webhook, consulta Clave de idempotencia más abajo.

Eventos de retiro

  • Name
    withdrawal.pending
    Type
    Description

    Una solicitud de retiro está pendiente de aprobación.

  • Name
    withdrawal.executed
    Type
    Description

    La transacción de retiro se envió a la blockchain.

  • Name
    withdrawal.confirmed
    Type
    Description

    La transacción de retiro se confirmó en la blockchain.

  • Name
    withdrawal.failed
    Type
    Description

    El retiro falló. El payload es el objeto de retiro con status: "FAILED". El objeto anidado transaction es opcional y puede ser null.

Cuando un retiro falla, los fondos retenidos se liberan de vuelta al saldo de la billetera. Puedes crear un nuevo retiro de forma segura.


Verificación de webhooks

Cada solicitud de webhook incluye una firma en el encabezado x-webhook-signature. Debes verificar esta firma para asegurarte de que el webhook proviene de Fystack.

Encabezados

  • Name
    x-webhook-signature
    Type
    string
    Description

    Firma Ed25519 del payload JSON canonicalizado.

  • Name
    x-webhook-event
    Type
    string
    Description

    El tipo de evento que activó este webhook.

Payload del webhook

  • Name
    webhook_id
    Type
    string
    Description

    El UUID de la configuración del webhook.

  • Name
    resource_id
    Type
    string
    Description

    El UUID del recurso que activó el webhook.

  • Name
    url
    Type
    string
    Description

    La URL a la que se envió el webhook.

  • Name
    event
    Type
    string
    Description

    El tipo de evento (por ejemplo, deposit.confirmed).

  • Name
    payload
    Type
    object
    Description

    Los datos específicos del evento.

Clave de idempotencia

Los webhooks de depósito (deposit.pending y deposit.confirmed) pueden entregarse más de una vez para el mismo depósito, por ejemplo tras un reintento después de un timeout o una interrupción temporal de tu endpoint. Usa siempre idempotency_key para deduplicar el procesamiento, en lugar de solo resource_id o id.

Para los eventos de depósito, el campo payload.idempotency_key identifica de forma única un evento de depósito subyacente. Como las condiciones de red o la caída de tu endpoint pueden hacer que Fystack vuelva a entregar el mismo webhook, debes tratar dos entregas con la misma idempotency_key como el mismo evento y procesarlas solo una vez.

  • Name
    idempotency_key
    Type
    string
    Description

    Una clave única que identifica este evento de depósito. Guarda las claves que ya procesaste (por ejemplo, en una base de datos con una restricción de unicidad) y omite el procesamiento si la idempotency_key de un webhook entrante ya se registró.

Verificación de firmas

Para verificar las firmas de webhook, necesitas:

  1. Canonicalizar el payload JSON (garantizar un orden consistente de las claves del objeto)
  2. Verificar la firma Ed25519 con tu clave pública

Canonicaliza siempre el payload del evento antes de verificarlo. Distintos serializadores JSON pueden producir representaciones de texto diferentes para la misma estructura de datos.

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

El paso de canonicalización es fundamental: garantiza que los mismos datos JSON siempre produzcan la misma representación de texto, sin importar cómo se formatearon u ordenaron originalmente.

Ejemplos de payloads de eventos

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

Registra tus endpoints de webhook en el portal de Fystack y selecciona qué eventos quieres recibir.