Remote API — business rules

The constraints, validation rules, and routing logic that govern the Scan to Pay Remote API.

These rules govern how the Remote API behaves at integration time and at runtime. For the call sequence itself, see Purchase flow; for the protocol overview, see Remote API overview.


Authentication and authorisation

RuleDetail
One auth mode per managerA manager profile is either Basic or Bearer — not both. Switching between them clears the existing credentials.
JWT is opt-inNew integrations should request Bearer + PKI from the Operations team. Existing Basic integrations continue working without forced migration.
TLS 1.2+ mandatoryHTTP is rejected at the load balancer. No exceptions.
Content-TypeAll POSTs must include Content-Type: application/json. Other content types are rejected with HTTP 400.
No session reuseEach request is independently authenticated. No cookies, no server-side session state.

For the full authentication walk-through, see Authentication.


MSISDN handling

The MSISDN is the wallet's primary identifier across the Remote API — it's effectively the user ID.

RuleDetail
International formatE.g. 27832006283. Leading + is accepted but stripped server-side. National-format (0832006283) is rejected.
Required on most requestsEvery lookup, purchase, state, history, and provisioning call. Only /menu and /binLookup are MSISDN-less.
One profile per MSISDN per wallet brandThe same MSISDN can have profiles across different wallet providers, but only one profile per provider.
clientIdentifier removedV4 removed support for clientIdentifier as a wallet ID. Use MSISDN.

Lookup behaviour

RuleDetail
URL-encode valuesIf value contains URL-special characters (e.g. a short URL), URL-encode before sending.
value is rewritten on responseThe value you sent on lookup may not be the value you send on purchase. Always use the value returned in the lookup response.
transactionId is single-useEach lookup generates a fresh transactionId. Don't reuse it across distinct payments.
NEED_INPUT means call againWhen status: "NEED_INPUT" is returned, prompt the user for the listed inputs and re-call generateTransactionId with the populated inputs array.
Inputs supportedcontact, shipping, billing, extraReference, extraInput, and per-VAS inputs. Anything beyond this list is unsupported and lookup will fail.
Static vs dynamic QRpoi: "11" = static (same QR reused for many transactions). poi: "12" = dynamic (single-use QR).
VAS menuAvailable via GET /menu only if the wallet brand is configured with VAS products by Operations.

Purchase authentication

The /purchaseTx request must include exactly one authentication block — either PIN data (AMT) or 3D Secure data — depending on what the card requires.

MethodRequired blockNotes
AMT (PIN)pinData.encryptedDataPIN must be encrypted using the Scan to Pay PinSecLib. Never send plain PIN.
3DS — fullsecureCodeData with cavv, xid, eciFlag, errorNo, paresStatus, signatureVerificationUse when your wallet has its own ACS integration.
3DS — basicsecureCodeBasic with secureCodeId and paResUse when you ran /secureCodeLookup and the customer completed the ACS challenge in a WebView.

Missing the required block, or sending both, results in HTTP 400 BAD_REQUEST.

The acceptable auth method for a card is exposed via the lookup response's acceptedPaymentMethods and via /binLookup's authMethod field.


Card data validation

RuleDetail
PAN length14-19 digits, Luhn-10 valid. Non-numeric or invalid Luhn → HTTP 400.
Expiry formatMMYY only. Past-dated expiry rejected at lookup.
CVVRequired when binLookup.cvvNeeded: true (most credit cards).
Account typeRequired when binLookup.accNeeded: true (most debit cards). Values: 10 (Savings), 20 (Current), 30 (Credit). Defaults to 30 if invalid.
Date of birthRequired when binLookup.dobNeeded: true. Format CCYYMMDD.
Cardholder nameRequired when binLookup.cardHolderNameNeeded: true. 3-26 characters.

Card details supplied to /purchaseTx are not auto-registered to a wallet. The wallet provider must explicitly use Card Provisioning to link a card.


Webhook vs polling

The Remote API supports both — but with rules.

RuleDetail
Webhook URL is per-purchasePass webhook on each /purchaseTx. It's not a profile-level config.
Webhook acknowledgementYour endpoint must respond HTTP 200 within 45 seconds or Scan to Pay auto-reverses the transaction. See Webhooks.
Polling is allowed alongside webhooksUnlike the Merchant API (where polling and webhooks are mutually exclusive), the Remote API allows both — the wallet UI polls for UX, the backend gets the webhook for accounting.
Final state arrives via either channelWhichever you read first, the answer is the same. The webhook payload and the /transactionState body are semantically equivalent.

VAS rules

RuleDetail
Menu is per-walletEach wallet brand has its own VAS configuration. The customer sees what your brand is configured for.
Menu structure is dynamicDon't hard-code menu IDs or names in your wallet UI — render them from the /menu response.
inputsRequired drives the UIWhen a VAS leaf has inputsRequired, prompt the customer accordingly. Validate against the supplied regex.
No VAS for unconfigured walletsIf /menu returns empty arrays, your wallet hasn't been provisioned with VAS — contact [email protected].

Transaction history

RuleDetail
Last 10 successful onlyThe history endpoint returns at most 10 most-recent successful transactions for the given MSISDN.
Not a ledgerThis is a wallet-UI convenience — don't use it as a source of truth for reconciliation. Maintain your own ledger from webhooks or polling.
Newest firstSorted descending by transaction date.

State semantics

Status patternWhat to do
SUCCESSShow success, store authCode and retrievalReferenceNumber for any future dispute.
PENDING / OFF_TO_BANK / OFF_TO_MASTERPASS / RECEIVED_*Transient. Keep polling.
END_BANK_NON_00 / BANK_REJECTEDBank declined. Check bankResponse against ISO response codes. Show a useful message to the customer.
END_INSUFFICIENT_FUNDS_OR_OVER_CREDIT_LIMITCard lacks funds. Don't retry without customer action.
END_REVERSED / END_REFUNDEDMoney returned to the customer. Update the wallet UI accordingly.
END_VALIDATION_FAILEDRequest was malformed at acquirer hop. Investigate; not the customer's problem.
END_TIMEOUT_*The customer or the network took too long. Allow retry.

Full list with descriptions in Transaction states.


Device data (optional but recommended)

The deviceData block isn't strictly required, but sending it improves fraud-screening and dispute-readiness:

FieldWhy send it
os, osVersion, appVersion, deviceDevice fingerprinting
serialHardware-level identifier for device-binding rules
latitude, longitudeVelocity / geographic fraud checks
clientIPCross-channel anomaly detection

Omit fields you can't capture — partial deviceData is fine. Never spoof values; the platform may flag implausible deltas (e.g. device that was in Cape Town 5 minutes ago suddenly transacting from Lagos).


Idempotency

The Remote API doesn't use a client-supplied idempotency key. Instead:

  • Lookup is naturally idempotent — repeated calls return fresh transactionIds, but it's cheap to call repeatedly.
  • Purchase is keyed by transactionId — a retry with the same transactionId and same card details is safe; the platform de-duplicates server-side.
  • State is read-only — call as often as you need.

If you need stricter idempotency semantics (e.g. retry-safe purchases across network failures), use /transactionState/{transactionId} to confirm before retrying /purchaseTx.


Error handling

HTTPBody statusWhat it means
200SUCCESSRequest completed successfully.
200PENDINGRequest accepted, processing async — poll for the result.
200NEED_INPUTLookup needs additional inputs from the customer.
200ERRORApplication-level error — read errorMessage.
400Malformed request — see errorMessage for which field.
401Bad credentials, expired JWT, or wrong auth mode for the manager.
500Internal server error. Safe to retry with the same transactionId.

For the full platform error code list, see Errors.


Rate limits and abuse prevention

There are no published per-request rate limits today, but the platform monitors for abuse patterns. See Rate limits for the canonical position. Specifically for the Remote API:

  • Lookup spam is the most-common anti-pattern — re-scanning the same QR every second. Implement client-side debouncing.
  • Polling cadence — every 1-2 seconds for the first 30 seconds is fine. Slower is fine. Faster than 1 second has no useful purpose and may trigger throttling.

What's next