Querying transactions
Look up the state of a transaction from your backend — when you don't have webhooks, when you missed a delivery, or when you're reconciling.
The Merchant Tx API lets your backend ask Scan to Pay "what happened to this transaction?" without relying on webhook delivery. It's the polling fallback for integrators that can't expose a public HTTPS endpoint, the recovery path when a webhook went missing, and the reconciliation tool when you're matching settled funds to your sales records.
The endpoints are grouped under /code/... and tagged Merchant Tx API in the OpenAPI reference.
For the rules and constraints, see Querying transactions — business rules.
When to query
| Scenario | What to call |
|---|---|
| No webhook configured for your merchant — you must poll | POST /code/queryRef |
| Webhook handler missed a delivery (outage, deploy gone wrong) | GET /code/transaction/state/{transactionId} — recovers without polling |
| Reconciling a specific transaction against your settlement file | GET /code/transaction/state/{transactionId} |
| Looking up a refund's state | POST /code/queryRef/refund |
| Debugging a customer's failed payment with their device info | POST /code/deviceInfo/{txId} |
| Finding a transaction by QR scan + amount (kiosk reconciliation) | POST /code/transaction/stateByQr |
Polling and webhooks are mutually exclusive. If your merchant has an HTTP notification URL configured in the Portal,queryRefreturns HTTP 406 Not Acceptable and you cannot poll for that merchant. Pick one path per merchant.
queryRef — poll by merchant reference
The standard polling endpoint. Returns the current state of a transaction keyed by the code + merchantReference you used at create time.
curl -X POST "https://qa.scantopay.io/pluto/code/queryRef" \
-u "$USERNAME:$PASSWORD" \
-H 'Content-Type: application/json' \
-d '{
"code": "0123456789",
"merchantReference": "demo-order-001"
}'Response
{
"transactionId": 81234,
"reference": "demo-order-001",
"amount": 10.00,
"currencyCode": "ZAR",
"code": "0123456789",
"status": "SUCCESS",
"clientMsisdn": "27831234567",
"retrievalReferenceNumber": 6321400012,
"authCode": "123456",
"bankResponse": "00",
"date": "2026-05-14T11:42:03Z"
}| Field | Description |
|---|---|
transactionId | Scan to Pay's internal transaction ID. Use this for follow-up reversal / refund calls. |
reference | The merchantReference you supplied at create time. |
amount | Final transaction amount (includes tip if any). |
currencyCode | ISO 4217 currency. |
code | The 10-digit code paid. |
status | The TxState value. SUCCESS is the only positive terminal value. See Transaction states. |
clientMsisdn | Customer's mobile in international format. |
retrievalReferenceNumber | Bank RRN — reconciliation key against your acquirer statement. |
authCode | Auth code from the issuer. |
bankResponse | ISO response code (e.g. 00, 51, 91) — see ISO response codes. |
date | ISO 8601 timestamp of the transaction. |
If the transaction hasn't reached the system yet (the customer hasn't paid), status returns N/A. Wait and retry; don't treat N/A as a failure.
getTransactionState — look up by transaction ID
When you already know the transactionId (from a previous webhook or queryRef call), use the GET endpoint. Simpler, no body, and works regardless of webhook configuration.
curl -X GET "https://qa.scantopay.io/pluto/code/transaction/state/81234" \
-u "$USERNAME:$PASSWORD"Returns the same payload shape as queryRef.
This is the recommended recovery endpoint if your webhook handler missed a delivery — it doesn't run into the polling-vs-webhook mutex.
queryRefund — look up a refund's state
Same shape as queryRef, but returns the state of the most recent refund attempt against the original transaction.
curl -X POST "https://qa.scantopay.io/pluto/code/queryRef/refund" \
-u "$USERNAME:$PASSWORD" \
-H 'Content-Type: application/json' \
-d '{
"code": "0123456789",
"merchantReference": "demo-order-001"
}'If multiple partial refunds have been processed against the same original transaction, this returns the most recent one. Reconcile by iterating against your refund records.
stateByQr — find transaction by QR + amount
Useful for kiosks and other touchpoints where you've got the QR code value and the amount, but not the merchantReference. Less commonly used than queryRef.
curl -X POST "https://qa.scantopay.io/pluto/code/transaction/stateByQr" \
-u "$USERNAME:$PASSWORD" \
-H 'Content-Type: application/json' \
-d '{
"code": "0123456789",
"amount": 10.00
}'Returns the same payload as queryRef. If multiple transactions match (same code paid at the same amount), returns the most recent.
getDeviceInfo — debugging
Returns the customer's device characteristics — IP address, OS, app version, GPS coordinates — for the given transactionId. Useful when investigating a customer-reported issue.
curl -X POST "https://qa.scantopay.io/pluto/code/deviceInfo/81234" \
-u "$USERNAME:$PASSWORD"Treat the returned data as PII; don't log it in plaintext.
Polling pattern
If you have no webhook configured and must poll:
async function pollForResult(code, merchantReference, maxAttempts = 24) {
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const result = await stp.queryRef({ code, merchantReference });
if (result.status === 'SUCCESS') return { success: true, ...result };
if (result.status.startsWith('END_')) return { success: false, ...result };
// N/A or transient state — keep polling
await sleep(5_000); // no faster than every 5 seconds
}
return { success: false, status: 'TIMEOUT' };
}- No faster than once every 5 seconds per transaction.
- Stop on any terminal state (
SUCCESSor anyEND_*). - Cap your total polling duration at ~2 minutes. Most transactions reach a terminal state within 30 seconds; longer than 2 minutes usually means the customer abandoned.
For production-quality integration, webhooks are still preferred. See Webhooks.
What's next
- Rules and constraints → Querying transactions — business rules
- Transaction state values you might see → Transaction states
- Map
bankResponsecodes to customer copy → ISO response codes - Use webhooks instead → Webhooks
- Polling cadence and rate limits → Rate limits
Updated 3 days ago
