Docs

Webhooks

Know when a payment is complete.

Add an HTTPS webhook endpoint in Merchant Dashboard. Quid Payments sends your server signed events when a checkout completes, fails, or expires.

Verify each event

Use the request body exactly as Quid Payments sent it.

Verify the original raw body before you parse the JSON.

Signature header
X-Payment-Platform-Signature: t=<unix_timestamp>,v1=<hex_hmac_sha256>

v1 = HMAC_SHA256(signing_secret, "<t>.<raw_request_body>")

Webhook checklist

Verify the event, then update the order.

Webhook handler checklist
1. Read the raw request body.
2. Parse t and v1 from X-Payment-Platform-Signature.
3. Reject timestamps more than 5 minutes from your server clock.
4. Recompute HMAC_SHA256(signing_secret, "<t>.<raw_request_body>").
5. Compare signatures with a constant-time comparison.
6. Store X-Payment-Platform-Event-Id and return 200 for already-processed events.
7. Treat delivery as at-least-once; your handler must be idempotent.
8. For checkout.session.completed, confirm amount_minor, currency,
   invoice_ref, and successful checkout status before fulfilment.

Verification examples

Verify the raw body before you parse it.

Use a five-minute timestamp tolerance. Compare signatures in constant time. Store each event ID before you update an order.

Node.js verification handler
import crypto from "node:crypto";

function verifyWebhook({ rawBody, signature, signingSecret }) {
  const parts = Object.fromEntries(signature.split(",").map((part) => part.split("=")));
  const timestamp = Number(parts.t);
  const received = parts.v1 || "";
  const ageSeconds = Math.abs(Date.now() / 1000 - timestamp);
  if (!timestamp || ageSeconds > 300) throw new Error("stale webhook");

  const expected = crypto.createHmac("sha256", signingSecret)
    .update(`${timestamp}.${rawBody}`).digest("hex");
  if (received.length !== expected.length ||
      !crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected))) {
    throw new Error("invalid webhook signature");
  }
}

export async function handleWebhook(request, response) {
  const rawBody = await getRawBody(request);
  verifyWebhook({ rawBody,
    signature: request.headers["x-payment-platform-signature"],
    signingSecret: process.env.PAYMENT_PLATFORM_WEBHOOK_SECRET });
  const eventId = request.headers["x-payment-platform-event-id"];
  if (await alreadyProcessed(eventId)) return response.status(200).end();
  const event = JSON.parse(rawBody);
  if (event.type === "checkout.session.completed") {
    await fulfilIfAmountCurrencyAndInvoiceMatch(event.data.session);
  } else if (event.type.startsWith("payout.")) {
    await refreshStoredPayout(event.data.payout.id, event.environment);
  }
  await markProcessed(eventId);
  response.status(200).end();
}
Headers

Read the event details

Read X-Payment-Platform-Event-Id, X-Payment-Platform-Event-Type, and X-Payment-Platform-Signature.

Events

Handle completed and unsuccessful checkouts

Handle checkout.session.completed, checkout.session.failed, and checkout.session.expired.

Order update

Confirm the payment

Confirm amount_minor, currency, invoice_ref, and successful checkout status before you fulfil.

Completed payment event
{
  "id": "evt_example",
  "type": "checkout.session.completed",
  "data": { "session": {
    "id": "cs_example", "status": "success", "amount_minor": 307038,
    "currency": "GHS", "invoice_ref": "INV-2026-001", "metadata": {}
  }}
}

Payout events

Read the payout subject and refresh your record.

Handle payout.requested, payout.processing, payout.paid, payout.failed, payout.cancelled and payout.returned. These events use data.payout, with environment and livemode at the top level.

The payout contains its ID, Quid reference, status, amount, fee, currency, bank reference, return reference and reconciliation status. It has no checkout session or recipient account details. See the complete payout event example.

Events go only to the active webhook paired with the creating key. Verify the raw body and timestamp using the same signature contract. Deduplicate by event id; retries reuse it. Do not assume arrival order. A paid payout can later be returned.

The example handlers call your own refreshStoredPayout function. It should match the payout ID and environment to your stored payout, fetch its current state with the creating key, and update your record before marking the event processed. Keep processing and event deduplication atomic or use a durable queue.