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

ScenarioWhat to call
No webhook configured for your merchant — you must pollPOST /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 fileGET /code/transaction/state/{transactionId}
Looking up a refund's statePOST /code/queryRef/refund
Debugging a customer's failed payment with their device infoPOST /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, queryRef returns 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"
}
FieldDescription
transactionIdScan to Pay's internal transaction ID. Use this for follow-up reversal / refund calls.
referenceThe merchantReference you supplied at create time.
amountFinal transaction amount (includes tip if any).
currencyCodeISO 4217 currency.
codeThe 10-digit code paid.
statusThe TxState value. SUCCESS is the only positive terminal value. See Transaction states.
clientMsisdnCustomer's mobile in international format.
retrievalReferenceNumberBank RRN — reconciliation key against your acquirer statement.
authCodeAuth code from the issuer.
bankResponseISO response code (e.g. 00, 51, 91) — see ISO response codes.
dateISO 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 (SUCCESS or any END_*).
  • 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