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
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
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 anidadotransactiones opcional y puede sernull.
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_keyde un webhook entrante ya se registró.
Verificación de firmas
Para verificar las firmas de webhook, necesitas:
- Canonicalizar el payload JSON (garantizar un orden consistente de las claves del objeto)
- 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.