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
}| Field | Type | Description |
|---|---|---|
| payment_session_id | string | The session reference returned from createPaymentSession. |
| status | string | Payment status: confirmed (paid) or failed. |
| timestamp | integer | When 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 });
}