Webhooks
Webhooks allow you to receive real-time notifications when specific events occur in your Fystack account.
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
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:
- Canonicalize the JSON payload (ensure consistent ordering of object keys)
- 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.