Webhooks are the backbone of real-time payment notifications. This guide covers how to secure your webhook endpoint and handle notifications reliably.
Understanding Webhook Signatures
Every webhook from Pulse2Pay includes two security headers:
|--------|-------------|
X-Pulse2Pay-SignatureX-Pulse2Pay-TimestampSignature Verification
Important: Webhook signatures use a different format than API request signatures!
// API Signature (for your requests to Pulse2Pay):
HMAC-SHA256(api_secret, timestamp + "." + method + "." + path + "." + body)
// Webhook Signature (for Pulse2Pay's requests to you):
HMAC-SHA256(webhook_secret, timestamp + "." + body)
The webhook secret is provided when you register a webhook endpoint.
Example Implementation (Node.js)
const crypto = require('crypto');function verifyWebhookSignature(req, webhookSecret) {
const signature = req.headers['x-pulse2pay-signature'];
const timestamp = req.headers['x-pulse2pay-timestamp'];
// Use raw body for accurate signature verification
const body = req.rawBody || JSON.stringify(req.body);
// Verify timestamp is within 5 minutes (timestamp is in milliseconds)
const now = Date.now();
if (Math.abs(now - parseInt(timestamp)) > 300000) {
throw new Error('Timestamp too old - possible replay attack');
}
// Compute expected signature (webhook format: timestamp.body)
const payload = ${timestamp}.${body};
const expected = crypto
.createHmac('sha256', webhookSecret)
.update(payload)
.digest('hex');
// Constant-time comparison
if (!crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
)) {
throw new Error('Invalid signature');
}
return true;
}
Replay Attack Prevention
Always verify the timestamp:
X-Timestamp headerRetry Logic
Pulse2Pay retries failed webhooks with exponential backoff:
|---------|-------|
A webhook is considered successful if your server returns a 2xx status code within 30 seconds. After 5 failed retry attempts, the webhook will be marked as failed.
Idempotent Processing
Your webhook handler must be idempotent. The same webhook may be delivered multiple times due to:
Idempotency Best Practices
async function handlePaymentWebhook(payment) {
// Use the payment ID as an idempotency key
const existing = await db.getPayment(payment.id);
if (existing && existing.status === payment.status) {
// Already processed - return success
return { success: true, duplicate: true };
}
// Process the payment
await db.updatePayment(payment.id, {
status: payment.status,
tx_hash: payment.tx_hash,
processed_at: new Date()
});
return { success: true };
}
Webhook Events
|-------|-------------|
payment.createdpayment.pendingpayment.confirmedpayment.underpaidpayment.overpaidpayment.expiredpayment.failedpayment.canceledTesting Webhooks
Use our sandbox environment to test webhooks before going live: