API Reference

Webhooks

When a payment reaches a final state, Scan & Pay sends a signed status notification to your server.

A notification can be delayed or missed; read the status before you complete an order.

Notification payload #

A status notification carries the session reference (payment_session_id), the current payment status, and the event timestamp:

POST notification
json
{
  "order_id": "order_456",
  "payment_session_id": "SP_SESS_abc123def456",
  "status": "confirmed",
  "timestamp": 1761878400
}
FieldTypeDescription
payment_session_idstringThe session reference returned from createPaymentSession.
statusstringPayment status: confirmed (paid) or failed.
timestampintegerWhen the notification was sent, in Unix seconds.

Verifying the signature #

To verify a notification, compute the HMAC-SHA256 hex digest of the raw request body using your Webhook Secret, and verify that it matches the X-Scanpay-Signature header.

TypeScript — route handler
ts
import crypto from 'node:crypto';

export async function POST(req: Request) {
  const rawBody = await req.text(); // raw body string
  const signature = req.headers.get('x-scanpay-signature') ?? '';

  const expected = crypto
    .createHmac('sha256', process.env.SCANANDPAY_WEBHOOK_SECRET!)
    .update(rawBody)
    .digest('hex');

  const sigBuf = Buffer.from(signature, 'hex');
  const expBuf = Buffer.from(expected, 'hex');

  if (sigBuf.length !== expBuf.length || !crypto.timingSafeEqual(sigBuf, expBuf)) {
    return new Response('Invalid signature', { status: 401 });
  }

  const event = JSON.parse(rawBody);
  const now = Math.floor(Date.now() / 1000);
  if (!event.timestamp || Math.abs(now - event.timestamp) > 60) {
    return new Response('Invalid timestamp', { status: 401 });
  }

  if (event.status === 'confirmed') {
    // Mark the order paid
  }

  return new Response('OK', { status: 200 });
}