Integrate
Webhooks
Kardlane tells your system what happened by POSTing signed events to your webhook URL.
Headers#
| Header | What it’s for |
|---|---|
X-Kardlane-Signature | Proves the request came from Kardlane: t=<unix>,v1=<hex>. |
X-Kardlane-Delivery | Unique id of this event. Deliveries are at-least-once — dedupe on it. |
Content-Type | application/json |
Verify the signature#
Compute HMAC-SHA256 over "<t>.<raw request body>" with your whsec_… signing secret, compare it to v1 in constant time, and reject anything older than five minutes. Use the raw body bytes — not re-serialised JSON.
import crypto from 'node:crypto';
/**
* X-Kardlane-Signature: t=<unix seconds>,v1=<hex>
* v1 = HMAC-SHA256(key = your whsec_… secret, message = "<t>.<raw body>")
*/
export function verifyKardlaneSignature(rawBody, header, secret, toleranceSecs = 300) {
if (!rawBody || !header || !secret) return false;
const parts = Object.fromEntries(header.split(',').map((kv) => kv.trim().split('=')));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSecs) return false;
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(String(parts.v1 ?? ''));
return a.length === b.length && crypto.timingSafeEqual(a, b);
}The envelope#
trade.succeeded
{
"id": "0f3c9a52-8d6e-4f0e-9b7a-2c1d5e8f4a10",
"apiVersion": "2026-09-01",
"type": "trade.succeeded",
"createdAt": "2026-10-03T09:21:07.512Z",
"tenantId": "b1e2c3d4-…",
"data": {
"tradeId": "6abea25d-…",
"externalRef": "sale_8213",
"verdict": "succeeded",
"cardType": "APPLE_ITUNES",
"country": "USD",
"value": 100,
"customerRate": 1080,
"vendorRate": 1150,
"margin": 70,
"vendor": "Vendor Alpha",
"idempotencyKey": "…"
}
}data.externalRef is your own id — use it to find the sale on your side. margin is the vendor rate minus your customer rate, rounded to two decimals.
Events#
| Event | When |
|---|---|
| trade.succeeded | The vendor redeemed and paid. Pay your customer. |
| trade.failed | The vendor rejected the card — with a customer-safe reason. |
| trade.escalated | A person needs to decide; carries the agent’s reading. |
| trade.cancelled | Closed as handled outside Kardlane. |
| connection.disconnected | A WhatsApp number dropped its session. |
| billing.* | Your prepaid balance crossed a threshold. |
Retries#
- Respond with any
2xxwithin a few seconds. Anything else — or a timeout — is retried. - Retries back off from 30 seconds up to 6 hours, for up to 12 attempts, always with the same delivery id.
- If deliveries keep failing, your alert recipients get a WhatsApp alert. Past deliveries and their responses are on each trade’s page.
Warning: Do the slow work (paying out, emailing) after you’ve answered 200 — or make your handler idempotent, because a timeout means the same event comes again.