Payments API

Two endpoints, one HTTP header, one JSON body. Your backend creates a session at checkout, renders the QR, then either polls for status or waits for our signed webhook confirmation. Your server mints the session, our pay app collects the payment, our webhook confirms back to you.

Your backend ── createSession() ─▶ Scan & Pay API (mint QR + pay URL) Customer phone ── scans QR ─▶ pay.scanandpay.com.au (we collect payment) Scan & Pay ── signed webhook ─▶ Your backend (payment confirmation)

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.
amountCentsintegeryes*Total in cents (e.g. 1990 for $19.90). Preferred.
amountnumberyes*Total in AUD dollars (e.g. 19.90). Deprecated — use amountCents. *Provide one or the other.
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

curl
bash
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",
    "amountCents": 1990,
    "payId": "merchant@example.com.au",
    "merchantName": "Acme Coffee"
  }'
PHP — scanandpay/php
php
use ScanAndPay\ScanAndPay;

$client = new ScanAndPay(
    merchantId: getenv('SCANANDPAY_MERCHANT_ID'),
    apiSecret: getenv('SCANANDPAY_API_SECRET'),
    baseUrl: getenv('SCANANDPAY_API_BASE_URL') ?: 'https://api.scanandpay.com.au',
);

$session = $client->createSession(
    amountCents: 1990,
    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/node
ts
import { ScanAndPay } from '@scanandpay/node';

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

const session = await sp.createSession({
  amountCents: 1990,
  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 OK
json
{
  "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",
  "amountCents": 1990,
  "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_.
curl
bash
curl "https://api.scanandpay.com.au/getPaymentStatus?sessionId=SP_SESS_abc123" \
  -H "X-Scanpay-Key: $SCANANDPAY_API_SECRET"

Response (200)

200 OK
json
{
  "success": true,
  "sessionId": "SP_SESS_abc123def456",
  "status": "PAID",
  "ui_state": "GREEN",
  "amountCents": 1990,
  "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.

POST /createRefund

Initiate a refund against a settled payment session. Partial refunds are supported — amountCents may be less than the original. Idempotent on idempotencyKey.

Request body

FieldTypeReq.Description
merchantIdstringyesYour Merchant ID. Must own the payment session.
paymentSessionIdstringyesThe SP_SESS_ identifier of the settled payment.
amountCentsintegeryesRefund amount in cents. Must be ≤ the original payment amount minus any prior refunds.
reasonstringnoReason for the refund (e.g. customer_request, duplicate).
idempotencyKeystringnoDedup key. SDKs generate a UUIDv7 if omitted.

Examples

curl
bash
curl https://api.scanandpay.com.au/createRefund \
  -X POST \
  -H "X-Scanpay-Key: $SCANANDPAY_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "merchantId": "xxxxxxxxxxxxxxxxxxxx",
    "paymentSessionId": "SP_SESS_abc123def456",
    "amountCents": 1990,
    "reason": "customer_request"
  }'
PHP — scanandpay/php
php
$refund = $client->createRefund(
    paymentSessionId: $session->sessionId,
    amountCents: 1990,
    reason: 'customer_request',
);

// $refund->refundId  — SP_RF_...
// $refund->status    — INITIATED
TypeScript — @scanandpay/node
ts
const refund = await sp.createRefund({
  paymentSessionId: session.sessionId,
  amountCents: 1990,
  reason: 'customer_request',
});

// refund.refundId  → SP_RF_...
// refund.status    → INITIATED

Response (200)

200 OK
json
{
  "success": true,
  "refundId": "SP_RF_k1T9DsNSoHnahZIB6",
  "status": "INITIATED",
  "amountCents": 1990,
  "originalInitiationId": "SP_PI_Zatem4pQCfFA",
  "paymentSessionId": "SP_SESS_abc123def456",
  "idempotencyKey": "019746ab-..."
}

Preconditions: The payment must be SETTLED and the PayTo agreement must still be ACTIVE. Cumulative refunds cannot exceed the original payment amount.

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