Receive real-time notifications for Styx events
Webhooks allow your application to receive real-time notifications when events occur on the Styx protocol. Instead of polling, you'll receive HTTP POST requests to your endpoint as events happen.
Pro Feature: Webhooks are available on Pro and Enterprise plans.Upgrade now
import express from 'express';
import crypto from 'crypto';
const app = express();
app.use(express.raw({ type: 'application/json' }));
const WEBHOOK_SECRET = process.env.STYX_WEBHOOK_SECRET!;
app.post('/webhooks/styx', (req, res) => {
const signature = req.headers['x-styx-signature'] as string;
const timestamp = req.headers['x-styx-timestamp'] as string;
// Verify signature
const payload = `${timestamp}.${req.body.toString()}`;
const expectedSignature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(payload)
.digest('hex');
if (!crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Verify timestamp is within 5 minutes
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(timestamp)) > 300) {
return res.status(401).json({ error: 'Timestamp too old' });
}
// Process the event
const event = JSON.parse(req.body.toString());
console.log('Received event:', event.type);
switch (event.type) {
case 'message.received':
handleNewMessage(event.data);
break;
case 'airdrop.claimed':
handleAirdropClaim(event.data);
break;
// ... handle other events
}
// Always return 200 quickly
res.json({ received: true });
});
app.listen(3000);A new private message was received for a registered address
{
"type": "message.received",
"timestamp": "2024-01-15T10:30:00Z",
"data": {
"signature": "5yN4...",
"slot": 242567890,
"recipient": "8Bfv...",
"sender": "3Kxj...",
"instructionTag": 3,
"encryptedPayload": "base64..."
}
}A recipient claimed tokens from a WhisperDrop campaign
{
"type": "airdrop.claimed",
"timestamp": "2024-01-15T10:30:00Z",
"data": {
"signature": "4kM7...",
"campaignId": "abc123...",
"claimant": "7Yxn...",
"amount": 1000000000,
"stealthClaim": true
}
}A new WhisperDrop campaign was initialized
{
"type": "campaign.created",
"timestamp": "2024-01-15T10:30:00Z",
"data": {
"signature": "2bNj...",
"campaignPDA": "9Abc...",
"authority": "8Bfv...",
"tokenMint": "EPjF...",
"expiresAt": "2024-02-15T10:30:00Z"
}
}A WhisperDrop campaign expired and tokens were reclaimed
{
"type": "campaign.expired",
"timestamp": "2024-01-15T10:30:00Z",
"data": {
"signature": "6kPq...",
"campaignPDA": "9Abc...",
"unclaimedTokens": 5000000000
}
}Always verify webhook signatures to ensure events are genuinely from Styx and haven't been tampered with.
Verify the signature
Every webhook includes an X-Styx-Signature header. Compute the expected HMAC and compare using a timing-safe function.
Check the timestamp
The X-Styx-Timestamp header contains a Unix timestamp. Reject webhooks older than 5 minutes to prevent replay attacks.
Use HTTPS only
Webhook endpoints must use HTTPS. We will not send events to HTTP endpoints.
Rotate secrets regularly
You can rotate your webhook secret from the dashboard. Both secrets will be valid for 24 hours during rotation.
If your endpoint doesn't respond with a 2xx status code, we'll retry with exponential backoff:
| Attempt | Delay | Time Since First Attempt |
|---|---|---|
| 1 | Immediate | 0s |
| 2 | 5 seconds | 5s |
| 3 | 30 seconds | 35s |
| 4 | 5 minutes | ~5.5min |
| 5 | 30 minutes | ~36min |
| 6 | 2 hours | ~2.5hrs |
After 6 failed attempts, the webhook will be marked as failed. You can view failed webhooks in your dashboard and manually retry them.
Return 200 quickly
Acknowledge the webhook with a 200 response before doing heavy processing. Use a queue for async processing.
Handle duplicates
In rare cases, webhooks may be delivered more than once. Use the event ID for idempotency.
Log all events
Log the full event payload for debugging. Make sure to filter sensitive data.
Handle timeouts
Webhooks timeout after 30 seconds. If your endpoint takes longer, we'll retry.