Webhooks
Debitura pushes real-time event notifications to your application via webhooks.
Signature Verification
Every webhook request includes an X-Debitura-Signature header for verification:
X-Debitura-Signature: t={timestamp},v1={signature}
| Component | Description |
|---|---|
t | Unix timestamp (seconds) when signed |
v1 | HMAC-SHA256 signature in lowercase hex |
How to Verify
- Extract timestamp and signature from the header
- Construct signed payload:
{timestamp}.{raw_json_body} - Decode secret: Base64 decode your webhook secret
- Compute HMAC-SHA256:
HMAC-SHA256(secret_bytes, signed_payload) - Compare signatures: Use constant-time comparison
- Validate timestamp: Reject if
|current_time - timestamp| > 300seconds
Use the RAW request body, not parsed/re-serialized JSON. Re-serialization may change whitespace or key ordering, breaking verification.
Implementation Example
const crypto = require('crypto');
function verifyWebhookSignature(rawBody, signatureHeader, webhookSecret) {
const parts = signatureHeader.split(',');
let timestamp, signature;
for (const part of parts) {
const [key, value] = part.split('=');
if (key === 't') timestamp = parseInt(value, 10);
else if (key === 'v1') signature = value;
}
if (!timestamp || !signature) return false;
// Validate timestamp (5-minute window)
const age = Math.floor(Date.now() / 1000) - timestamp;
if (age > 300 || age < -30) return false;
// Compute expected signature
const signedPayload = `${timestamp}.${rawBody}`;
const secretBytes = Buffer.from(webhookSecret, 'base64');
const expected = crypto
.createHmac('sha256', secretBytes)
.update(signedPayload, 'utf8')
.digest('hex');
// Constant-time comparison
try {
return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expected, 'hex')
);
} catch {
return false;
}
}
Common Mistakes
Not decoding Base64 secret
// Wrong: using Base64 string directly
crypto.createHmac('sha256', webhookSecret);
// Correct: decode first
crypto.createHmac('sha256', Buffer.from(webhookSecret, 'base64'));
Using parsed JSON body
// Wrong: re-serialized (formatting may differ)
const body = JSON.stringify(req.body);
// Correct: raw request body
app.use(express.raw({ type: 'application/json' }));
const rawBody = req.body.toString('utf8');
Middleware ordering (Express.js) — express.raw() must run before any JSON body parser for the webhook route. If you use app.use(express.json()) globally, register the webhook route first or use a dedicated router.
Skipping timestamp validation leaves you vulnerable to replay attacks.
Using standard string comparison leaks timing information. Always use constant-time comparison.
Retry Behavior
Debitura retries failed webhook deliveries with exponential backoff. Your endpoint should:
- Return
2xxwithin 10 seconds - Be idempotent (same event may be delivered multiple times)
- Process asynchronously if handling takes longer than 10 seconds
Integration-Specific Setup
For webhook configuration and event types specific to your integration: