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 |
The v1 value is a lowercase hexadecimal string — do NOT base64-decode it before comparing. Only the webhook secret itself is base64-encoded; the signature is always raw 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 Examples
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;
const age = Math.floor(Date.now() / 1000) - timestamp;
if (age > 300 || age < -30) return false;
const signedPayload = `${timestamp}.${rawBody}`;
const secretBytes = Buffer.from(webhookSecret, 'base64');
const expected = crypto
.createHmac('sha256', secretBytes)
.update(signedPayload, 'utf8')
.digest('hex');
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 up to 8 attempts. 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
If a delivery is failing, see Webhook Troubleshooting for error message reference.
Delivery order is not guaranteed
Each event is delivered as an independent message with its own retry schedule, so arrival order across events is not guaranteed. For example, a case close emits a case.updated event followed by a case.closed event, but a retry on the first can cause case.closed to arrive at your endpoint before case.updated.
Do not infer state from the order in which events arrive. Treat every event as independent and idempotent: key off the event id, and when you need the authoritative current state of a case, re-fetch it via the API rather than reconstructing it from the event sequence.
Local Development
To receive webhook deliveries on your local machine, expose a local port via a tunneling tool before creating your test subscription.
ngrok
# Expose port 3000
ngrok http 3000
ngrok prints a forwarding URL (e.g. https://abc123.ngrok.io). Use this as the endpoint URL when creating a test subscription. Set isTestMode: true so the subscription receives only test case events and never picks up production traffic.
cloudflared (alternative)
cloudflared tunnel --url http://localhost:3000
Cloudflare Tunnel provides a stable URL without requiring an ngrok account.
Identifying test deliveries
A test subscription (isTestMode: true) only ever receives events from test cases — production events are never delivered to it. You don't need a special header to distinguish test traffic: if your subscription is a test subscription, every delivery it receives is from a test case.
Delivery History
Every webhook delivery attempt is recorded and queryable. Use GET /webhooks/events to inspect what fired for a given case:
GET /webhooks/events?caseId={id}&since=2026-01-15T00:00:00Z
XApiKey: your_api_key
This is available on both the Customer API (https://customer-api.debitura.com) and the Collection Partner API (https://collectionpartner-api.debitura.com). For full details — including how to replay events and use the fire primitive — see the CI Testing guide.
Deprecated: Customer API POST /webhooks/{id}/replay
POST /webhooks/{id}/replay is deprecated and should not be used in new integrations. Use GET /webhooks/events?caseId={id} to inspect delivery history and POST /webhooks/events/{id}/replay to re-deliver a specific event.
Despite its name, POST /webhooks/{id}/replay does not replay events — it returns a count of cases created since a given timestamp (eventsReplayed). The endpoint is kept for backward compatibility. (An older colon form {id}:replay also works if the colon is percent-encoded as %3A, but the slash form is canonical.) See Delivery History & Replay in the Client webhook guide for context.
Test vs Live subscriptions
Every webhook subscription is either a test subscription or a live subscription, set by the isTestMode field. You can change it after the subscription is created — the classification is updated in place (no delete-and-recreate needed).
isTestMode | What events it receives |
|---|---|
false (default) | Events from live production cases only |
true | Events from test cases only |
Use a test subscription while building your integration — it receives events from test cases without interfering with your live production handler. Once your handler is working correctly, create a separate live subscription (isTestMode: false) for production traffic.
To switch a subscription between test and live mode, update isTestMode on the existing subscription — PATCH on the Customer API, or the update (PUT) endpoint on the partner APIs. The classification is re-applied in place; you do not need to delete and recreate the subscription.
Test subscriptions and live subscriptions are independent. You can run both at the same time — for example, a live subscription for production and a test subscription pointed at a staging endpoint.
Integration-Specific Setup
For webhook configuration and event types specific to your integration, see Client Webhooks, Referral Partner Webhooks, or Collection Partner Webhooks.