Webhooks
Receive real-time notifications when payment events occur.
Why webhooks?
Webhooks push data to your server as soon as a payment event happens. This is more reliable and faster than polling GET /v1/payment/:id in a loop.
Recommendation
Use the checkout.settled event to trigger fulfillment. Only query payment status directly when you need the current state for a UI display.
Setup
1. Create your endpoint
// Express.js — use raw body for signature verification
app.post('/webhooks/cloakd',
express.raw({ type: 'application/json' }),
(req, res) => {
// verify signature, process event, return 200
res.status(200).json({ received: true });
}
);2. Configure your webhook URL
curl -X PUT https://api.cloakd.ai/v1/webhooks/config \
-H "X-API-Key: sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{"webhookUrl": "https://yoursite.com/webhooks/cloakd", "enabled": true}'3. Get your webhook secret
curl -X GET https://api.cloakd.ai/v1/webhooks/secrets \ -H "X-API-Key: sk_live_your_api_key"
Store the secret securely — you need it to verify webhook signatures.
Event types
| Event | When it fires |
|---|---|
| checkout.created | A new payment checkout was created |
| checkout.pending | Payment detected on-chain, waiting for confirmations |
| checkout.processing | Payment being processed and converted |
| checkout.settled | Payment fully confirmed and settled — safe to fulfill |
| checkout.failed | Payment failed or checkout expired |
| checkout.refunded | A refund was issued |
Payload structure
All events share this envelope:
{
"id": "evt_1234567890",
"type": "checkout.settled",
"timestamp": "2026-01-03T14:30:00.000Z",
"data": {
"checkout": {
"id": "chk_abc123",
"status": "SETTLED",
"amount": "100.00",
"currency": "USD",
"cryptoAmount": "0.0025",
"cryptoCurrency": "BTC",
"settledAt": "2026-01-03T14:30:00.000Z"
},
"payment": {
"txHash": "abc123...",
"confirmations": 3,
"network": "bitcoin"
}
},
"metadata": {
"orderId": "order_12345"
}
}Signature verification
Every webhook includes two headers:
X-Webhook-SignatureHMAC-SHA256 signature of the payloadX-Webhook-TimestampUnix timestamp when the webhook was sent
The signature is computed as: HMAC-SHA256(timestamp + "." + raw_payload, your_secret)
Reject webhooks with timestamps older than 5 minutes to prevent replay attacks.
// Node.js verification
const crypto = require('crypto');
function verifySignature(rawBody, signature, timestamp, secret) {
const signedPayload = `${timestamp}.${rawBody.toString()}`;
const expected = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Use timing-safe comparison
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}Handling webhooks
app.post('/webhooks/cloakd',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.headers['x-webhook-signature'];
const timestamp = req.headers['x-webhook-timestamp'];
if (!verifySignature(req.body, signature, timestamp, WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Reject stale webhooks (> 5 min)
if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) {
return res.status(401).json({ error: 'Timestamp expired' });
}
const event = JSON.parse(req.body.toString());
switch (event.type) {
case 'checkout.settled':
fulfillOrder(event.data.checkout, event.metadata);
break;
case 'checkout.failed':
handleFailure(event.data.checkout, event.metadata);
break;
// Handle other events as needed
}
// Always return 200 to acknowledge receipt
res.status(200).json({ received: true });
}
);Always return 200
Return a 200 response as quickly as possible, even if your fulfillment logic is asynchronous. Process the event in a background job rather than making Cloakd wait for it.