Webhooks
Webhooks allow you to receive real-time notifications when events occur in your Cake Merchant account, such as when a payment confirms.
Event Types
charge.created— A new charge was createdcharge.pending— Payment detected in mempool, awaiting confirmationscharge.confirmed— Payment confirmed on the blockchain (at your configured minimum: 3, 5, or 10 confirmations)charge.expired— Charge expired without payment (1 hour default)charge.underpaid— Payment received but amount is less than requiredpayout.sent— XMR payout swept to merchant's payout wallet
Webhook Payload
All webhooks are sent as HTTP POST requests with JSON body:
POST https://yoursite.com/webhooks/cakemerchant
Content-Type: application/json
X-CAKEMERCHANT-SIGNATURE: <hmac-sha256-signature>
{
"event": "charge.confirmed",
"data": {
"id": "ch_abc123def456",
"amount": 25.00,
"currency": "USD",
"amount_xmr": 0.071428,
"subaddress": "84Hv16y6x7BTie3ib5Sx...",
"status": "confirmed",
"tx_hash": "7d2e4f8a1b3c5d6e...",
"confirmations": 10, // actual count at confirmation time
"metadata": {
"order_id": "12345",
"buyer_fields": "email,name",
"buyer_info": {
"email": "customer@example.com",
"name": "Jane Smith"
}
},
"created_at": "2026-02-15T10:00:00.000Z",
"confirmed_at": "2026-02-15T10:20:00.000Z"
},
"timestamp": "2026-02-15T10:20:00.000Z"
}
Signature Verification
Every webhook includes an X-CAKEMERCHANT-SIGNATURE
header containing an HMAC-SHA256 signature of the request body.
Always verify signatures to ensure webhooks are genuine:
Node.js
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
// Use timing-safe comparison
return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expected, 'hex')
);
}
// Express handler
app.post('/webhooks/cakemerchant', express.raw({type: 'application/json'}), (req, res) => {
const signature = req.headers['x-cakemerchant-signature'];
const payload = req.body.toString();
if (!verifyWebhookSignature(payload, signature, WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(payload);
switch (event.event) {
case 'charge.confirmed':
// Fulfill the order
fulfillOrder(event.data.metadata.order_id);
break;
case 'charge.expired':
// Handle expiration
cancelOrder(event.data.metadata.order_id);
break;
}
res.status(200).send('OK');
});
Python
import hmac
import hashlib
def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
# Flask handler
@app.route('/webhooks/cakemerchant', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-CAKEMERCHANT-SIGNATURE')
payload = request.get_data()
if not verify_signature(payload, signature, WEBHOOK_SECRET):
return 'Invalid signature', 401
event = request.get_json()
if event['event'] == 'charge.confirmed':
fulfill_order(event['data']['metadata']['order_id'])
return 'OK', 200
PHP
<?php
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_CAKEMERCHANT_SIGNATURE'];
$secret = 'your_webhook_secret';
$expected = hash_hmac('sha256', $payload, $secret);
if (!hash_equals($expected, $signature)) {
http_response_code(401);
exit('Invalid signature');
}
$event = json_decode($payload, true);
if ($event['event'] === 'charge.confirmed') {
fulfillOrder($event['data']['metadata']['order_id']);
}
echo 'OK';
Webhook Secret
When you create a webhook, you receive a secret key. Store this securely - it's only shown once. If you lose it, delete and recreate the webhook.
Retry Policy
If your endpoint returns a non-2xx status code or the connection fails, we'll retry with exponential backoff:
- 1st retry: 5 seconds
- 2nd retry: 10 seconds
After 3 total attempts (initial + 2 retries), the delivery is marked as failed. All delivery attempts are logged in the webhook delivery log on your dashboard.
Security Best Practices
- Always verify signatures: Never process a webhook without validating the HMAC signature. Attackers can forge webhook payloads to trigger false fulfillments.
- Use timing-safe comparison: Use
crypto.timingSafeEqual(Node.js),hmac.compare_digest(Python), orhash_equals(PHP) to prevent timing attacks. - Validate charge status independently: After receiving a webhook, query
GET /api/charges/:idto confirm the status server-side before fulfilling orders. - Handle duplicates: Use the charge ID as an idempotency key. You may receive the same event more than once.
- Respond quickly: Return 200 within 10 seconds. Queue long-running tasks.
- Use HTTPS: Webhook URLs must use HTTPS in production.
- Don't trust metadata blindly: Metadata is merchant-supplied. Validate it against your own records.
- Buyer fields are buyer-supplied: Data in
metadata.buyer_infocomes from the customer. Treat it as untrusted input — validate and sanitize before use.
Testing Webhooks
Use services like webhook.site or ngrok to test webhooks during development.