SwiftPay signs webhook events using a hash-based message authentication code (HMAC) with SHA-256. The signature is sent in the X-Webhook-Signature header.
The signature header contains a timestamp and one or more signatures. The timestamp is prefixed by t=, and each signature is prefixed by a scheme. Schemes start with v, followed by an integer. Currently, the only valid signature scheme is v1.Example Header:
Signature Generation: The signed payload is hashed using HMAC-SHA256 with your webhook secret as the key
Header Construction: The timestamp and signature are combined into the format shown above
const crypto = require('crypto');function verifyWebhookSignature(payload, signatureHeader, secret) {// 1. Extract timestamp and signature from headerconst parts = signatureHeader.split(',');const timestamp = parts.find(p => p.startsWith('t=')).split('=')[1];const signature = parts.find(p => p.startsWith('v1=')).split('=')[1];// 2. Check timestamp is recent (within 5 minutes) to prevent replay attacksconst currentTime = Math.floor(Date.now() / 1000);if (Math.abs(currentTime - parseInt(timestamp)) > 300) {return false;}// 3. Create the signed payloadconst signedPayload = `${timestamp}.${payload}`;// 4. Calculate expected signatureconst expectedSignature = crypto.createHmac('sha256', secret).update(signedPayload).digest('hex');// 5. Compare signatures using constant-time comparisonreturn crypto.timingSafeEqual(Buffer.from(signature, 'hex'),Buffer.from(expectedSignature, 'hex'));}
import hmacimport hashlibimport timedef verify_signature(payload, signature_header, secret): # 1. Extract timestamp and signature from header parts = signature_header.split(',') timestamp = next(p.split('=')[1] for p in parts if p.startswith('t=')) signature = next(p.split('=')[1] for p in parts if p.startswith('v1=')) # 2. Check timestamp is recent (within 5 minutes) current_time = int(time.time()) if abs(current_time - int(timestamp)) > 300: return False # 3. Create the signed payload signed_payload = f"{timestamp}.{payload.decode()}" # 4. Calculate expected signature expected_signature = hmac.new( secret.encode(), signed_payload.encode(), hashlib.sha256 ).hexdigest() # 5. Compare signatures return hmac.compare_digest(signature, expected_signature)
Webhooks may be sent multiple times. Use the eventId to ensure idempotent processing:
async function processWebhook(event) { // Check if already processed const exists = await db.webhookEvents.findOne({ eventId: event.eventId }); if (exists) return; // Process and mark as handled await handleEvent(event); await db.webhookEvents.create({ eventId: event.eventId });}
Use HTTPS
Always use HTTPS endpoints. HTTP endpoints are rejected.
Monitor Failures
Monitor webhook delivery status in your dashboard and set up alerts for failures.