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);
  }
  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": {}
  }}
}