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

Terminal
bash
npm install @scanandpay/node

Requires Node.js 18.0.0 or higher.

Credentials

Open Business dashboard → Settings → Integrations and copy these four values into your backend environment.

LabelEnvironment variable
Merchant IDSCANANDPAY_MERCHANT_ID
API Base URLSCANANDPAY_API_BASE_URL
API SecretSCANANDPAY_API_SECRET
Webhook SecretSCANANDPAY_WEBHOOK_SECRET
.env
bash
SCANANDPAY_MERCHANT_ID=merchant_xxxxxxxxxxxxxxxxxxxx
SCANANDPAY_API_BASE_URL=https://api.scanandpay.com.au
SCANANDPAY_API_SECRET=sk_live_xxxxxxxxxxxxxxxxxxxx
SCANANDPAY_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxx

Quickstart (Backend)

server.ts
ts
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.

CheckoutPage.tsx
tsx
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, ... });        // ✗ ValidationError

For display, use session.amount directly:

const display = session.amount.toFixed(2);  // "19.90"

Error handling

All thrown errors extend ScanAndPayError.

ClassWhen
ValidationErrorBad input rejected before any HTTP call.
AuthenticationErrorAPI rejected API Secret (rotate your keys).
ApiErrorNon-2xx response from the API.
NetworkErrorTransport failure after exhausting retries.
WebhookSignatureErrorWebhook 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.

  1. Create your local order first. Store cart, customer, amount, currency, and a durable platformOrderId in your database with a pending payment status.
  2. 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.
  3. 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.
  4. Expose a small status endpoint. Point pollUrl at your backend. It should call client.getStatus(sessionId) and return only the public status fields your UI needs.
  5. Verify webhooks on raw request bytes. Use client.webhooks.verify(signature, rawBody) before trusting the payload.
  6. Mark paid from the verified webhook. event.status === 'confirmed' is the source of truth for fulfilment.
  7. 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):

  1. Create the order BEFORE minting the session. The session binds to your platformOrderId and the webhook references the same id when the payment is confirmed. Persist the order in your DB (status pending) first, then call client.createSession({...}).
  2. 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.
  3. 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.