APIs
Webhooks
Let Payer come to you. Register a signed HTTPS endpoint and get a POST the moment a checkout session changes state.
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:
| Event | Sent when |
|---|---|
checkout.session.created | A checkout session was created. |
checkout.session.completed | Payment settled. |
checkout.session.failed | A confirmation attempt failed terminally. |
checkout.session.cancelled | The session was cancelled. |
checkout.session.expired | The 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:
{
"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
| Field | Type | Description |
|---|---|---|
id | string | Event id, prefixed evt_. Stable across every endpoint that receives it — use it to de-duplicate. |
type | string | The event name from the table above. |
api_version | string | The payload schema version (2026-07-01). |
created | integer | Unix timestamp of the event. |
data.object | object | The checkout session snapshot. |
The session object
| Field | Type | Notes |
|---|---|---|
amount | integer | In laari (minor units) here — unlike the GraphQL query. |
currency | string | Always MVR. |
status | string | INITIATED · COMPLETED · CANCELLED · EXPIRED. |
payment | object | Present only once the session is COMPLETED. Carries the payment id and completed_at. |
merchant | object | The 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
| Header | Example | Description |
|---|---|---|
Payer-Signature | t=1753600000, v1=8f4e2c1a… | Timestamped HMAC. See below. |
Payer-Event-Id | evt_2f8c1a9b-… | Same id as id in the body. |
Payer-Event-Type | checkout.session.completed | Same as type in the body. |
User-Agent | Payer-Webhooks/1.0 | Fixed. |
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: t=1753600000, v1=8f4e2c1a9b7d6e5f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3fThe 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.Recompute the HMAC over the raw request body — before any JSON parsing, over the exact bytes Payer sent.
- 2.Compare it to
v1in constant time. - 3.Reject deliveries whose
tis 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.
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.
| Setting | Type | Notes |
|---|---|---|
url | string (https) | Must start with https://. Max 2048 characters. |
events | array | One or more event types from the catalogue. |
description | string | A label to recognise the endpoint. |
environment | live · test | Which 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.