Skip to main content

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}
ComponentDescription
tUnix timestamp (seconds) when signed
v1HMAC-SHA256 signature in lowercase hex

How to Verify

  1. Extract timestamp and signature from the header
  2. Construct signed payload: {timestamp}.{raw_json_body}
  3. Decode secret: Base64 decode your webhook secret
  4. Compute HMAC-SHA256: HMAC-SHA256(secret_bytes, signed_payload)
  5. Compare signatures: Use constant-time comparison
  6. Validate timestamp: Reject if |current_time - timestamp| > 300 seconds
Critical

Use the RAW request body, not parsed/re-serialized JSON. Re-serialization may change whitespace or key ordering, breaking verification.

Implementation Example

Node.js
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 2xx within 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: