API/Webhooks

Webhooks

Webhooks are the source of truth for whether an order is paid. Link's onSuccess and the public token exchange are fast paths; a webhook still arrives if the shopper closed their browser or your server missed the answer. Register endpoints in the dashboard under Webhooks.

Events

EventWhen
session.authorizedThe shopper approved a plan; funds are authorized (captured later).
session.capturedThe order is paid. Mark it paid (once per payment_id).
session.declinedThe provider declined.
session.cancelledThe payment was cancelled.
session.expiredThe checkout expired unpaid.
credit.installment_paidCoverPay plan: a later payment (2 to 4) was collected.
credit.installment_failedCoverPay plan: a payment failed; it retries after 24 hours.

Payload

{
  "id": "evt_3UO1…",
  "type": "session.captured",
  "created": "2026-10-07T21:05:00.000Z",
  "data": {
    "payment_id": "pi_3UO1…",        // exchange returns the same id
    "order_id": "order_123",          // from your link token
    "amount": 12000,
    "currency": "USD",
    "provider": "zip",
    "session_status": "captured"
  }
}
Treat the event as a hint
Before marking an order paid, read the payment back with your key:GET /v1/bnpl/sessions/{payment_id}. Check the order id and amount match your order.

Verify the signature

Every delivery has X-CoverPay-Signature-V2: t=<unix seconds>,v1=<hex>, an HMAC-SHA256 of"<t>.<raw body>" with your endpoint's signing secret. Verify it over the raw body before parsing, and reject timestamps more than 5 minutes old.

Node
import crypto from 'crypto';

function verifyCoverPay(rawBody: string, header: string | null, secret: string): boolean {
  if (!header) return false;
  const parts = header.split(',').map((p) => p.trim().split('='));
  const t = Number(parts.find(([k]) => k === 't')?.[1]);
  const v1s = parts.filter(([k]) => k === 'v1').map(([, v]) => v);
  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > 300) return false;
  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  return v1s.some((v) => v.length === expected.length && crypto.timingSafeEqual(Buffer.from(v), Buffer.from(expected)));
}

The older X-CoverPay-Signature (HMAC of the body without a timestamp) is deprecated. It's still sent for now so existing integrations keep working; the date it stops will be announced in the changelog. Verify V2.

Delivery and retries

Answer 2xx once you've recorded the event. Any other answer, or no answer within the timeout, is retried after 1 minute, 5 minutes and 30 minutes. Events can arrive more than once and out of order: record each payment_id once.

Checklist

  • Verify X-CoverPay-Signature-V2 before parsing; 401 if it fails.
  • Read the payment back with your key; check order id and amount.
  • Record once per payment_id (the same order can also arrive through the public token exchange).
  • Return 2xx quickly; do slow work afterwards.