Payments API

Two endpoints, one HTTP header, one JSON body. Create a session at checkout, render the QR, then either poll for status or wait for the signed webhook to land.

Base URL: https://api.scanandpay.com.au
Auth header: X-Scanpay-Key: <your API Secret>
Currency: AUD only · Amounts: AUD major units (e.g. 19.90 = $19.90) · Session expiry: 5 minutes

POST /createPaymentSession

Creates a payment session and returns the QR + payment URL. Idempotent on (merchantId, platformOrderId) — reposting the same pair returns the existing session.

Request body

FieldTypeReq.Description
merchantIdstringyesYour Merchant ID from the dashboard.
platformOrderIdstringyesYour unique order reference. Idempotency key.
amountnumberyesTotal in AUD (e.g. 19.90). Must be > 0.
payIdstringyesYour registered PayID (email, phone, or ABN).
merchantNamestringyesDisplay name on the customer payment screen.
currencystringnoDefaults to AUD. Only AUD is currently supported.
referencestringnoCustomer-visible label. Defaults to Order #{platformOrderId}.
sourceenumnoOne of api · woocommerce · shopify · magento · pos · kiosk. Analytics-only.

Examples

curlbash
curl https://api.scanandpay.com.au/createPaymentSession \
  -X POST \
  -H "X-Scanpay-Key: $SCANANDPAY_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "merchantId": "xxxxxxxxxxxxxxxxxxxx",
    "platformOrderId": "order_456",
    "amount": 19.90,
    "payId": "merchant@example.com.au",
    "merchantName": "Acme Coffee"
  }'
PHP — scanandpay/phpphp
use ScanAndPay\ScanAndPay;

$client = new ScanAndPay(
    merchantId: getenv('SCANANDPAY_MERCHANT_ID'),
    apiSecret: getenv('SCANANDPAY_API_SECRET'),
);

$session = $client->createSession(
    amount: 19.90,
    platformOrderId: 'order_456',
    payId: 'merchant@example.com.au',
    merchantName: 'Acme Coffee',
);

// $session->qrUrl    — base64 PNG, render in <img src=...>
// $session->payUrl   — hosted payment fallback
// $session->sessionId
// $session->expiresAt
TypeScript — @scanandpay/nodets
import { ScanAndPay } from '@scanandpay/node';

const sp = new ScanAndPay({
  merchantId: process.env.SCANANDPAY_MERCHANT_ID!,
  apiSecret: process.env.SCANANDPAY_API_SECRET!,
});

const session = await sp.createSession({
  amount: 19.90,
  platformOrderId: 'order_456',
  payId: 'merchant@example.com.au',
  merchantName: 'Acme Coffee',
});

// session.qrUrl     → data:image/png;base64,...
// session.payUrl    → https://pay.scanandpay.com.au/p/SP_SESS_...
// session.sessionId → SP_SESS_...
// session.expiresAt → ISO 8601 string

Response (200)

200 OKjson
{
  "success": true,
  "sessionId": "SP_SESS_abc123def456",
  "payUrl": "https://pay.scanandpay.com.au/p/SP_SESS_abc123def456",
  "qrUrl": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...",
  "payId": "merchant@example.com.au",
  "amount": 19.90,
  "currency": "AUD",
  "reference": "Order #456 | SP_SESS_abc123def456",
  "status": "WAITING",
  "ui_state": "AMBER",
  "expiresAt": "2026-04-30T12:35:00.000Z"
}

GET /getPaymentStatus

Read the current state of a session. Use this when polling — but for server-to-server flows webhooks are preferred.

Query parameters

FieldTypeDescription
sessionIdstringSession identifier returned from createPaymentSession. Must start with SP_SESS_.
curlbash
curl "https://api.scanandpay.com.au/getPaymentStatus?sessionId=SP_SESS_abc123" \
  -H "X-Scanpay-Key: $SCANANDPAY_API_SECRET"

Response (200)

200 OKjson
{
  "success": true,
  "sessionId": "SP_SESS_abc123def456",
  "status": "PAID",
  "ui_state": "GREEN",
  "amount": 19.90,
  "currency": "AUD",
  "payId": "merchant@example.com.au",
  "reference": "Order #456 | SP_SESS_abc123def456",
  "merchantName": "Acme Coffee",
  "createdAt": "2026-04-30T12:30:00.000Z",
  "paidAt": "2026-04-30T12:32:14.000Z",
  "expiresAt": "2026-04-30T12:35:00.000Z"
}

Status values

statusui_stateTerminalMeaning
WAITINGAMBERNoCustomer hasn't paid yet — keep polling.
PAIDGREENYesFunds confirmed via the bank rail.
EXPIREDREDYes5-minute window elapsed without payment.
FAILEDREDYesBank rejected or session aborted.

Polling cadence: 2 seconds is a good default. Stop as soon as status becomes PAID, EXPIRED, or FAILED. Back off exponentially on 5xx errors.

Errors

StatusWhenBody shape
400Missing field, non-positive amount, currency != AUD, or invalid sessionId format.{ error, required? }
401Missing or invalid X-Scanpay-Key.{ error }
403Merchant subscription not active.{ error: "SUBSCRIPTION_REQUIRED", message, reason }
404Session not found.{ error, sessionId }
405Wrong HTTP method.{ error }
500Internal server error.{ error, message? }

Idempotency

Reposting the same platformOrderId for the same merchant returns the existing session, not a fresh one. This is intentional — your checkout can safely retry on transient errors without creating duplicate sessions.

Once a session expires, reposting the same platformOrderId still returns the expired record. To start fresh, change the order reference (or append a retry suffix).

Next

  • Webhooks — receive signed payment events instead of polling
  • OpenAPI spec — full machine-readable contract