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.
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. |
| amountCents | integer | yes* | Total in cents (e.g. 1990 for $19.90). Preferred. |
| amount | number | yes* | Total in AUD dollars (e.g. 19.90). Deprecated — use amountCents. *Provide one or the other. |
| 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",
"amountCents": 1990,
"payId": "merchant@example.com.au",
"merchantName": "Acme Coffee"
}'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->expiresAtimport { 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 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",
"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
| 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",
"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
| 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.
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
| Field | Type | Req. | Description |
|---|---|---|---|
| merchantId | string | yes | Your Merchant ID. Must own the payment session. |
| paymentSessionId | string | yes | The SP_SESS_ identifier of the settled payment. |
| amountCents | integer | yes | Refund amount in cents. Must be ≤ the original payment amount minus any prior refunds. |
| reason | string | no | Reason for the refund (e.g. customer_request, duplicate). |
| idempotencyKey | string | no | Dedup key. SDKs generate a UUIDv7 if omitted. |
Examples
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"
}'$refund = $client->createRefund(
paymentSessionId: $session->sessionId,
amountCents: 1990,
reason: 'customer_request',
);
// $refund->refundId — SP_RF_...
// $refund->status — INITIATEDconst refund = await sp.createRefund({
paymentSessionId: session.sessionId,
amountCents: 1990,
reason: 'customer_request',
});
// refund.refundId → SP_RF_...
// refund.status → INITIATEDResponse (200)
{
"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
| 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