Node.js SDK
Official Node.js SDK for Scan & Pay — accept PayTo PayID payments via QR code 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 float dollars.
const session = await client.createSession({
amount: 19.90, // $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 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
amount is always float dollars (e.g. 19.90for $19.90). This matches the Scan & Pay API directly — no multiplication or division needed.
await client.createSession({ amount: 19.90, ... }); // ✓ $19.90
await client.createSession({ amount: 0.50, ... }); // ✓ $0.50
await client.createSession({ amount: 1000.00, ... }); // ✓ $1,000.00
await client.createSession({ amount: -1, ... }); // ✗ ValidationError
await client.createSession({ amount: 0, ... }); // ✗ ValidationErrorFor display, use session.amount directly:
const display = session.amount.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 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 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 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.
Resources
Need help with your integration?
Book a session with Mehmet directly — SDK setup, terminal configuration, or any technical questions.