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
| Rule | Detail |
|---|---|
| One auth mode per manager | A manager profile is either Basic or Bearer — not both. Switching between them clears the existing credentials. |
| JWT is opt-in | New integrations should request Bearer + PKI from the Operations team. Existing Basic integrations continue working without forced migration. |
| TLS 1.2+ mandatory | HTTP is rejected at the load balancer. No exceptions. |
| Content-Type | All POSTs must include Content-Type: application/json. Other content types are rejected with HTTP 400. |
| No session reuse | Each 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.
| Rule | Detail |
|---|---|
| International format | E.g. 27832006283. Leading + is accepted but stripped server-side. National-format (0832006283) is rejected. |
| Required on most requests | Every lookup, purchase, state, history, and provisioning call. Only /menu and /binLookup are MSISDN-less. |
| One profile per MSISDN per wallet brand | The same MSISDN can have profiles across different wallet providers, but only one profile per provider. |
| clientIdentifier removed | V4 removed support for clientIdentifier as a wallet ID. Use MSISDN. |
Lookup behaviour
| Rule | Detail |
|---|---|
| URL-encode values | If value contains URL-special characters (e.g. a short URL), URL-encode before sending. |
| value is rewritten on response | The 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-use | Each lookup generates a fresh transactionId. Don't reuse it across distinct payments. |
| NEED_INPUT means call again | When status: "NEED_INPUT" is returned, prompt the user for the listed inputs and re-call generateTransactionId with the populated inputs array. |
| Inputs supported | contact, shipping, billing, extraReference, extraInput, and per-VAS inputs. Anything beyond this list is unsupported and lookup will fail. |
| Static vs dynamic QR | poi: "11" = static (same QR reused for many transactions). poi: "12" = dynamic (single-use QR). |
| VAS menu | Available 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.
| Method | Required block | Notes |
|---|---|---|
| AMT (PIN) | pinData.encryptedData | PIN must be encrypted using the Scan to Pay PinSecLib. Never send plain PIN. |
| 3DS — full | secureCodeData with cavv, xid, eciFlag, errorNo, paresStatus, signatureVerification | Use when your wallet has its own ACS integration. |
| 3DS — basic | secureCodeBasic with secureCodeId and paRes | Use 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
| Rule | Detail |
|---|---|
| PAN length | 14-19 digits, Luhn-10 valid. Non-numeric or invalid Luhn → HTTP 400. |
| Expiry format | MMYY only. Past-dated expiry rejected at lookup. |
| CVV | Required when binLookup.cvvNeeded: true (most credit cards). |
| Account type | Required when binLookup.accNeeded: true (most debit cards). Values: 10 (Savings), 20 (Current), 30 (Credit). Defaults to 30 if invalid. |
| Date of birth | Required when binLookup.dobNeeded: true. Format CCYYMMDD. |
| Cardholder name | Required 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.
| Rule | Detail |
|---|---|
| Webhook URL is per-purchase | Pass webhook on each /purchaseTx. It's not a profile-level config. |
| Webhook acknowledgement | Your endpoint must respond HTTP 200 within 45 seconds or Scan to Pay auto-reverses the transaction. See Webhooks. |
| Polling is allowed alongside webhooks | Unlike 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 channel | Whichever you read first, the answer is the same. The webhook payload and the /transactionState body are semantically equivalent. |
VAS rules
| Rule | Detail |
|---|---|
| Menu is per-wallet | Each wallet brand has its own VAS configuration. The customer sees what your brand is configured for. |
| Menu structure is dynamic | Don't hard-code menu IDs or names in your wallet UI — render them from the /menu response. |
| inputsRequired drives the UI | When a VAS leaf has inputsRequired, prompt the customer accordingly. Validate against the supplied regex. |
| No VAS for unconfigured wallets | If /menu returns empty arrays, your wallet hasn't been provisioned with VAS — contact [email protected]. |
Transaction history
| Rule | Detail |
|---|---|
| Last 10 successful only | The history endpoint returns at most 10 most-recent successful transactions for the given MSISDN. |
| Not a ledger | This 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 first | Sorted descending by transaction date. |
State semantics
| Status pattern | What to do |
|---|---|
SUCCESS | Show 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_REJECTED | Bank declined. Check bankResponse against ISO response codes. Show a useful message to the customer. |
END_INSUFFICIENT_FUNDS_OR_OVER_CREDIT_LIMIT | Card lacks funds. Don't retry without customer action. |
END_REVERSED / END_REFUNDED | Money returned to the customer. Update the wallet UI accordingly. |
END_VALIDATION_FAILED | Request 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:
| Field | Why send it |
|---|---|
os, osVersion, appVersion, device | Device fingerprinting |
serial | Hardware-level identifier for device-binding rules |
latitude, longitude | Velocity / geographic fraud checks |
clientIP | Cross-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 sametransactionIdand 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
| HTTP | Body status | What it means |
|---|---|---|
200 | SUCCESS | Request completed successfully. |
200 | PENDING | Request accepted, processing async — poll for the result. |
200 | NEED_INPUT | Lookup needs additional inputs from the customer. |
200 | ERROR | Application-level error — read errorMessage. |
400 | — | Malformed request — see errorMessage for which field. |
401 | — | Bad credentials, expired JWT, or wrong auth mode for the manager. |
500 | — | Internal 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
- Step-by-step call walk-through → Purchase flow
- Endpoint reference → Remote API overview
- Link cards into the wallet → Card Provisioning
- All transaction states → Transaction states
- Webhook protocol → Webhooks
Updated 2 days ago
