Node.js SDK
Official Node.js SDK for Scan & Pay — accept PayTo PayID payments via QR Pass from any Node.js environment (Express, NestJS, Next.js, etc.). Includes React components for the frontend.
Install
npm install @scanandpay/nodeRequires Node.js 18.0.0 or higher.
Credentials
Open Business dashboard → Settings → Integrations and copy these four values into your backend environment.
| Label | Environment variable |
|---|---|
| Merchant ID | SCANANDPAY_MERCHANT_ID |
| API Base URL | SCANANDPAY_API_BASE_URL |
| API Secret | SCANANDPAY_API_SECRET |
| Webhook Secret | SCANANDPAY_WEBHOOK_SECRET |
SCANANDPAY_MERCHANT_ID=merchant_xxxxxxxxxxxxxxxxxxxx
SCANANDPAY_API_BASE_URL=https://api.scanandpay.com.au
SCANANDPAY_API_SECRET=sk_live_xxxxxxxxxxxxxxxxxxxx
SCANANDPAY_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxQuickstart (Backend)
import { ScanAndPay } from '@scanandpay/node';
const client = new ScanAndPay({
merchantId: process.env.SCANANDPAY_MERCHANT_ID!,
apiBaseUrl: process.env.SCANANDPAY_API_BASE_URL!,
apiSecret: process.env.SCANANDPAY_API_SECRET!,
webhookSecret: process.env.SCANANDPAY_WEBHOOK_SECRET, // optional
});
// 1. Create a session at checkout. Amount is integer cents.
const session = await client.createSession({
amountCents: 1990, // $19.90
platformOrderId: 'order_456',
payId: 'merchant@example.com.au',
merchantName: 'Acme Coffee',
});
// 2. Verify and consume webhooks (Express example).
import { WebhookSignatureError } from '@scanandpay/node';
app.post('/webhooks/scanandpay', async (req, res) => {
try {
const signature = req.headers['x-scanpay-signature'] as string;
const event = client.webhooks.verify(signature, req.body);
if (event.status === 'confirmed') {
// Mark order paid using event.order_id, event.tx_id, ...
}
res.json({ received: true });
} catch (err) {
if (err instanceof WebhookSignatureError) {
return res.status(401).send('Webhook verification failed');
}
throw err;
}
});Quickstart (React)
The Node.js SDK includes a React component for rendering the QR Pass checkout widget on your frontend.
import { CheckoutWidget } from '@scanandpay/node/react';
function CheckoutPage({ session }) {
return (
<CheckoutWidget
session={session}
pollUrl="/api/scanandpay/status"
onSuccess={(sessionId) => { window.location.href = '/thank-you'; }}
theme="light"
/>
);
}Amount format
amountCents is always integer cents (e.g. 1990 for $19.90). Pass cents into the SDK directly. Divide by 100 only when formatting for display.
await client.createSession({ amountCents: 1990, ... }); // ✓ $19.90
await client.createSession({ amountCents: 50, ... }); // ✓ $0.50
await client.createSession({ amountCents: 100000, ... }); // ✓ $1,000.00
await client.createSession({ amountCents: -1, ... }); // ✗ ValidationError
await client.createSession({ amountCents: 0, ... }); // ✗ ValidationErrorFor display, divide cents by 100 at the UI boundary:
const display = (session.amountCents / 100).toFixed(2); // "19.90"Error handling
All thrown errors extend ScanAndPayError.
| Class | When |
|---|---|
| ValidationError | Bad input rejected before any HTTP call. |
| AuthenticationError | API rejected API Secret (rotate your keys). |
| ApiError | Non-2xx response from the API. |
| NetworkError | Transport failure after exhausting retries. |
| WebhookSignatureError | Webhook signature or timestamp check failed. |
Production integration checklist
Use the SDK from your backend only. The browser can display the returned QR Pass or pay URL, but it must never receive your API Secret or Webhook Secret.
- Create your local order first. Store cart, customer, amount, currency, and a durable
platformOrderIdin your database with a pending payment status. - Create one Scan & Pay session for that order. Call
client.createSession()from your backend using the same order id. Pass an idempotency key based on your order id when retries are possible. - Render the payment step as pending. Show the returned session with
CheckoutWidget, your own QR Pass renderer, or a redirect to the returned pay URL. Do not send the customer to a success page yet. - Expose a small status endpoint. Point
pollUrlat your backend. It should callclient.getStatus(sessionId)and return only the public status fields your UI needs. - Verify webhooks on raw request bytes. Use
client.webhooks.verify(signature, rawBody)before trusting the payload. - Mark paid from the verified webhook.
event.status === 'confirmed'is the source of truth for fulfilment. - Keep test and live credentials separate. Never commit merchant credentials into source control, frontend bundles, mobile apps, logs, screenshots, or support tickets.
Safe to share publicly: package install commands, SDK method names, request and response fields, webhook verification rules, idempotency guidance, test-mode behaviour, and error handling. Keep database schemas, internal routes, cloud project names, merchant secrets, and admin tooling private.
Integration pitfalls
Three things that bite first-time integrators (we've hit each one ourselves):
- Create the order BEFORE minting the session. The session binds to your
platformOrderIdand the webhook references the same id when the payment is confirmed. Persist the order in your DB (statuspending) first, then callclient.createSession({...}). - Don't finalise the order on Place Order. A common pattern in checkout code is a hardcoded list — "if this gateway is stripe / paypal / etc. defer; otherwise finalise immediately". If that list is missing
scanandpay, the checkout flashes a success screen and the<CheckoutWidget>never renders. Always treat Scan & Pay as a deferred / async-confirmation gateway, just like Stripe Checkout or PayPal — the customer needs to see the QR Pass and complete the scan-and-pay in their banking app before the order can be considered paid. - Webhook is the source of truth. Mark the order paid only when the verified webhook arrives with
event.status === 'confirmed'. The Place Order click on your site does NOT mean money has moved.