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
| Field | Type | Req. | Description |
|---|---|---|---|
| merchantId | string | yes | Your Merchant ID from the dashboard. |
| platformOrderId | string | yes | Your unique order reference. Idempotency key. |
| amount | number | yes | Total in AUD (e.g. 19.90). Must be > 0. |
| payId | string | yes | Your registered PayID (email, phone, or ABN). |
| merchantName | string | yes | Display name on the customer payment screen. |
| currency | string | no | Defaults to AUD. Only AUD is currently supported. |
| reference | string | no | Customer-visible label. Defaults to Order #{platformOrderId}. |
| source | enum | no | One of api · woocommerce · shopify · magento · pos · kiosk. Analytics-only. |
Examples
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"
}'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->expiresAtimport { 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 stringResponse (200)
{
"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
| Field | Type | Description |
|---|---|---|
| sessionId | string | Session identifier returned from createPaymentSession. Must start with SP_SESS_. |
curl "https://api.scanandpay.com.au/getPaymentStatus?sessionId=SP_SESS_abc123" \
-H "X-Scanpay-Key: $SCANANDPAY_API_SECRET"Response (200)
{
"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
| status | ui_state | Terminal | Meaning |
|---|---|---|---|
| WAITING | AMBER | No | Customer hasn't paid yet — keep polling. |
| PAID | GREEN | Yes | Funds confirmed via the bank rail. |
| EXPIRED | RED | Yes | 5-minute window elapsed without payment. |
| FAILED | RED | Yes | Bank 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
| Status | When | Body shape |
|---|---|---|
| 400 | Missing field, non-positive amount, currency != AUD, or invalid sessionId format. | { error, required? } |
| 401 | Missing or invalid X-Scanpay-Key. | { error } |
| 403 | Merchant subscription not active. | { error: "SUBSCRIPTION_REQUIRED", message, reason } |
| 404 | Session not found. | { error, sessionId } |
| 405 | Wrong HTTP method. | { error } |
| 500 | Internal 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