Browse documentation

APIs

Webhooks

Let Payer come to you. Register a signed HTTPS endpoint and get a POST the moment a checkout session changes state.

View as Markdown

Rather than poll Retrieve a session for a status change, register an endpoint and let Payer come to you. When a checkout session reaches a lifecycle event, Payer sends a signed POST to your HTTPS URL. Each delivery carries a Payer-Signature header you verify with the endpoint's signing secret.

Endpoints are managed from the Developers page in your Payer dashboard (admin role) — one set per environment, not with the checkout API keys. Test endpoints receive events from your _test_ sessions, live endpoints from _live_ sessions.

Events

Subscribe each endpoint to one or more of these events:

EventSent when
checkout.session.createdA checkout session was created.
checkout.session.completedPayment settled.
checkout.session.failedA confirmation attempt failed terminally.
checkout.session.cancelledThe session was cancelled.
checkout.session.expiredThe session's 24-hour window elapsed unpaid.

The delivery payload

Every delivery is a JSON envelope. The data.object is a snapshot of the checkout session at the moment the event fired:

checkout.session.completed
{
  "id": "evt_2f8c1a9b-7d6e-4f0a-b1c2-3d4e5f6a7b8c",
  "type": "checkout.session.completed",
  "api_version": "2026-07-01",
  "created": 1753600000,
  "data": {
    "object": {
      "id": "9b1c8f2e-3d4a-4f11-a0c2-1e2d3c4b5a69",
      "reference": "order_10482",
      "amount": 2490,
      "currency": "MVR",
      "status": "COMPLETED",
      "title": "Order #10482",
      "description": null,
      "payment": {
        "id": "6a5b4c3d-2e1f-4a0b-9c8d-7e6f5a4b3c2d",
        "completed_at": 1753600000
      },
      "merchant": {
        "id": "42",
        "name": "Acme Store"
      }
    }
  }
}

Envelope fields

FieldTypeDescription
idstringEvent id, prefixed evt_. Stable across every endpoint that receives it — use it to de-duplicate.
typestringThe event name from the table above.
api_versionstringThe payload schema version (2026-07-01).
createdintegerUnix timestamp of the event.
data.objectobjectThe checkout session snapshot.

The session object

FieldTypeNotes
amountintegerIn laari (minor units) here — unlike the GraphQL query.
currencystringAlways MVR.
statusstringINITIATED · COMPLETED · CANCELLED · EXPIRED.
paymentobjectPresent only once the session is COMPLETED. Carries the payment id and completed_at.
merchantobjectThe receiving merchant's id (stringified) and name.

Note

A payload never contains tokens, secrets, wallet ids or card data. Treat it as a notification: fetch anything sensitive yourself, authenticated, using the ids it carries.

Delivery headers

HeaderExampleDescription
Payer-Signaturet=1753600000, v1=8f4e2c1a…Timestamped HMAC. See below.
Payer-Event-Idevt_2f8c1a9b-…Same id as id in the body.
Payer-Event-Typecheckout.session.completedSame as type in the body.
User-AgentPayer-Webhooks/1.0Fixed.

Verifying signatures

Each endpoint has its own signing secret — a whsec_ value shown once when you register the endpoint or rotate the secret, and never retrievable again. Payer signs every delivery with it and sets the Payer-Signature header:

Payer-Signature header
Payer-Signature: t=1753600000, v1=8f4e2c1a9b7d6e5f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f

The header carries a Unix timestamp t and a signature v1, the hex HMAC-SHA256 of ${t}.${raw_body} keyed with your signing secret. To verify:

  1. 1.Recompute the HMAC over the raw request body — before any JSON parsing, over the exact bytes Payer sent.
  2. 2.Compare it to v1 in constant time.
  3. 3.Reject deliveries whose t is more than five minutes from now, to blunt replays.

During a secret rotation the header may carry more than one v1 value — a match against any one is valid.

Verify a delivery (Node.js)
import { createHmac, timingSafeEqual } from "node:crypto";

const TOLERANCE_SECONDS = 300; // reject deliveries older than 5 minutes

/**
 * Verify a Payer-Signature header against the raw request body.
 * Header shape: t=<unix>, v1=<hex>[, v1=<hex>...].
 * Verify before JSON.parse — sign over the exact bytes Payer sent.
 */
export function verifyPayerSignature(rawBody, header, secret) {
  let timestamp = null;
  const signatures = [];
  for (const part of header.split(",")) {
    const [key, value] = part.trim().split("=");
    if (key === "t") timestamp = Number(value);
    else if (key === "v1" && value) signatures.push(value);
  }
  if (!Number.isInteger(timestamp) || signatures.length === 0) return false;
  if (Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) return false;

  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");
  const a = Buffer.from(expected);

  // Accept a match against any v1 entry (constant-time comparison).
  return signatures.some((sig) => {
    const b = Buffer.from(sig);
    return a.length === b.length && timingSafeEqual(a, b);
  });
}

Responding, retries and replay

Return any 2xx status to acknowledge a delivery. Anything else — or a timeout past 10 seconds — is treated as a failure and retried.

  • Payer retries with a growing backoff: after 1, 5 and 15 minutes, then 1, 3, 6 and 12 hours — up to 8 attempts spanning roughly a day.
  • Because retries and de-duplication are real, your handler must be idempotent: key on the evt_ id and ignore one you've already processed.
  • Every attempt is recorded in the delivery log on the Developers page, with the response status and body snippet. You can resend any delivery from there; it's re-signed and re-sent verbatim.

Tip

Acknowledge fast, work later. Verify the signature, enqueue the event id for background processing, and return 200 immediately — don't do slow work inside the request Payer is timing.

Managing endpoints

From the dashboard you can register an endpoint, choose its events, enable or disable it without losing its configuration, rotate its signing secret, and delete it.

SettingTypeNotes
urlstring (https)Must start with https://. Max 2048 characters.
eventsarrayOne or more event types from the catalogue.
descriptionstringA label to recognise the endpoint.
environmentlive · testWhich environment's events it receives. Set once.

Rotating a secret issues a new whsec_ immediately; update your endpoint before the old one stops verifying. Deleting an endpoint revokes its secret at once.