Overview
Webhooks allow you to receive real-time notifications when settlement status changes, eliminating the need for polling.
Setup
Provide a webhook_url when verifying a payment:
{
"transaction_hash": "0x...",
"chain": "base",
"seller_address": "0x...",
"expected_amount": "1000000000000000000",
"webhook_url": "https://your-app.com/webhooks/settlement"
}
Webhook Payload
When settlement completes, we’ll POST to your webhook URL:
{
"event_type": "settlement.completed",
"event_id": "evt_abc123def456",
"timestamp": "2025-01-15T10:35:00Z",
"verification_id": "ver_abc123def456",
"settlement_id": "set_xyz789ghi012",
"status": "settled",
"data": {
"settlement_tx_hash": "0xabcdef...",
"settled_amount": "1000000000000000000",
"fee": "50000000000000000"
},
"signature": "hmac_sha256_signature_here"
}
Event Types
Payment verification completed successfully
Payment verification failed
Settlement completed successfully
Signature Verification
CRITICAL: Always verify webhook signatures to ensure requests are from 0xmeta.ai
Extract Signature
Get the signature from the X-Webhook-Signature header
Compute Expected Signature
Use HMAC-SHA256 with your webhook secret
Compare Signatures
Use constant-time comparison to prevent timing attacks
const crypto = require("crypto");
function verifyWebhook(payload, signature, secret) {
// Serialize payload to canonical JSON
const payloadStr = JSON.stringify(payload, Object.keys(payload).sort(), ":");
// Compute expected signature
const expected = crypto
.createHmac("sha256", secret)
.update(payloadStr)
.digest("hex");
// Constant-time comparison
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
// In your webhook handler
app.post("/webhooks/settlement", (req, res) => {
const signature = req.headers["x-webhook-signature"];
const payload = req.body;
if (!verifyWebhook(payload, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).json({ error: "Invalid signature" });
}
// Process webhook
console.log("Settlement completed:", payload.settlement_id);
res.json({ received: true });
});
Retry Logic
If your webhook endpoint fails, we’ll retry with exponential backoff:
| Attempt | Delay |
|---|
| 1 | 1 minute |
| 2 | 5 minutes |
| 3 | 15 minutes |
| 4 | 30 minutes |
| 5 | 1 hour |
After 5 failed attempts, the webhook is marked as failed.
Best Practices
- Acknowledge receipt immediately
- Process webhook asynchronously
- Don’t perform long operations in webhook handler
app.post('/webhooks', async (req, res) => {
// Verify signature
if (!verifyWebhook(...)) {
return res.status(401).end();
}
// Acknowledge immediately
res.status(200).json({ received: true });
// Process asynchronously
processWebhook(req.body).catch(console.error);
});
Store event IDs to prevent duplicate processing:const processedEvents = new Set();
app.post('/webhooks', (req, res) => {
const eventId = req.body.event_id;
if (processedEvents.has(eventId)) {
return res.status(200).json({ received: true });
}
// Process webhook
processWebhook(req.body);
processedEvents.add(eventId);
res.status(200).json({ received: true });
});
- Webhook URLs must use HTTPS in production - Use valid SSL certificates -
Don’t use self-signed certificates
- Log all webhook deliveries
- Alert on failed deliveries
- Monitor response times
- Track processing errors
Testing Webhooks Locally
Use ngrok to expose your local server:
# Start ngrok
ngrok http 3000
# Use the ngrok URL in your API request
{
"webhook_url": "https://abc123.ngrok.io/webhooks/settlement"
}
Or use webhook.site to inspect payloads:
# Get a unique URL
https://webhook.site/your-unique-id
# Use it in your request
{
"webhook_url": "https://webhook.site/your-unique-id"
}
Webhook Flow Diagram
Never expose your webhook secret in client-side code or version control.
Webhooks timeout after 10 seconds. Ensure your endpoint responds quickly.