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
| Event | When |
|---|---|
session.authorized | The shopper approved a plan; funds are authorized (captured later). |
session.captured | The order is paid. Mark it paid (once per payment_id). |
session.declined | The provider declined. |
session.cancelled | The payment was cancelled. |
session.expired | The checkout expired unpaid. |
credit.installment_paid | CoverPay plan: a later payment (2 to 4) was collected. |
credit.installment_failed | CoverPay 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"
}
}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.
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-V2before 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.
