Kardlane
Integrate

Webhooks

Kardlane tells your system what happened by POSTing signed events to your webhook URL.

Headers#

HeaderWhat it’s for
X-Kardlane-SignatureProves the request came from Kardlane: t=<unix>,v1=<hex>.
X-Kardlane-DeliveryUnique id of this event. Deliveries are at-least-once — dedupe on it.
Content-Typeapplication/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#

EventWhen
trade.succeededThe vendor redeemed and paid. Pay your customer.
trade.failedThe vendor rejected the card — with a customer-safe reason.
trade.escalatedA person needs to decide; carries the agent’s reading.
trade.cancelledClosed as handled outside Kardlane.
connection.disconnectedA WhatsApp number dropped its session.
billing.*Your prepaid balance crossed a threshold.

Retries#

  • Respond with any 2xx within 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.