Portal API — business rules
Cross-cutting rules that apply to every Portal API endpoint — authorisation, ownership, state requirements, audit logging, and error semantics.
These rules apply across every Portal API endpoint, regardless of which operation you're calling. The per-endpoint pages document the specific call shape; this page documents what's true for all of them.
For the operations themselves, start at Portal API overview.
Authentication and authorisation
| Rule | Detail |
|---|---|
| TLS 1.2+ mandatory | HTTP requests are rejected at the load balancer. No exceptions. |
| Basic Auth on every request | Authorization: Basic <base64(username:password)> — no session reuse, no cookies, no token caching server-side. |
ROLE_REMOTE opt-in required | The credentialled profile must have ROLE_REMOTE granted, or every API call returns 401. Granted by Scan to Pay Operations on request. |
| Caller-prefix routing | Username's prefix (PSP_, ACQUIRER_, MERCHANT_) determines what endpoints you can hit. See Authentication. |
| No session state | Each request is independently authenticated. No login flow, no token refresh, no cross-request state. |
Ownership validation
Every endpoint that references a specific merchant runs an ownership check.
| Caller | Ownership rule |
|---|---|
| PSP | Target merchant's pspId must match the caller's PSP ID |
| Acquirer | Target merchant's acquirer must match the caller's acquirer name |
| Merchant | Target merchant ID in the request must equal the caller's own merchant ID |
A failed ownership check returns:
HTTP 400 BAD_REQUEST
"Invalid 'merchantId'"
The same body is returned whether the merchant doesn't exist or just isn't yours — the platform doesn't disclose whether merchants exist outside your scope.
Merchant state requirements
Most write operations require the target merchant to be in a specific state:
| Operation | Required state |
|---|---|
| Update | ACTIVE |
| Suspend | ACTIVE |
| Unsuspend | SUSPENDED |
| Add / update notification (HTTP, Email, SMS) | ACTIVE |
| Rotate webhook key | ACTIVE |
| Rotate API password | ACTIVE |
| List notifications | ACTIVE |
| Generate Lib Lite token | ACTIVE |
| List Lib Lite tokens | ACTIVE |
Endpoints that don't enforce state:
| Operation | Why no state check |
|---|---|
| List merchants | Read-only across all states in your scope |
| Create | No existing merchant to check (creates the row) |
| Deactivate PayShap | Useful to clean up suspended merchants before termination |
| Transaction lookup | Historical data should be accessible regardless of current state |
| Transaction certificates | Audit access must survive merchant lifecycle |
A failed state check returns:
HTTP 400 BAD_REQUEST
"Merchant not in 'ACTIVE' state"
(or the relevant state for the operation).
Request format
| Rule | Detail |
|---|---|
Content-Type: application/json required | Other content types are rejected with HTTP 400. |
| JSON request bodies | Validated server-side. Unknown fields are silently ignored. Required-field omissions return 400 with a field-specific message. |
| Path variables are typed | /restful/merchant/activate/{merchantId} expects a numeric long; non-numeric path values return HTTP 400. |
| Path-variable IDs vs body-field IDs | Some endpoints take the merchant ID in the path (e.g. payshap/deactivate/{merchantId}); some take it in the body (e.g. suspend). Per-endpoint docs are explicit. |
Response format
| Rule | Detail |
|---|---|
| JSON responses by default | Except for the certificate endpoints, which return a String body containing a base64-encoded PDF. |
HTTP 200 OK for success | Including no-op idempotent successes (e.g. PayShap deactivate on a merchant that wasn't using PayShap). |
HTTP 200 OK with empty array | For list endpoints with no matching records. Not 404. |
| Plain-text error bodies | Most validation errors return a plain-string body — not a JSON error envelope. Some legacy endpoints return JSON; mostly it's plain text. Don't assume a structured error shape. |
Audit logging
Every state-changing operation is captured in the audit log with:
| Field | What's logged |
|---|---|
| Acting user | The authenticated principal (PSP, acquirer, or merchant identity) |
| Source | PORTAL_API_PSP, PORTAL_API_ACQUIRER, or PORTAL_API_MERCHANT — distinguishes API calls from Portal UI actions |
| Action type | The specific operation (e.g. ACTION_MERCHANT_UPDATE, ACTION_NOTIFY_UPDATE, ACTION_MERCHANT_API_PASSWORD_GENERATE) |
| Target | The merchant ID or other entity being acted on |
| Reason / detail | Free-text where applicable (e.g. suspend / unsuspend reasons) |
| Timestamp | UTC, captured server-side |
Audit logs are visible in the Portal UI's audit screens. They're not directly queryable via the Portal API — for audit log access, use the Portal UI or request an extract from [email protected].
Idempotency
The Portal API doesn't use client-supplied idempotency keys. Behaviour on repeat calls:
| Operation | Idempotency model |
|---|---|
| Create merchant | Not idempotent — repeat calls create duplicate merchant records. Use unique data per call. |
| Update merchant | Idempotent — same input, same result |
| Suspend / Unsuspend | Idempotent within state — repeat suspend on an already-suspended merchant returns 400, not duplicate suspension |
| Add notification (HTTP) | Idempotent if URL + version match existing — otherwise updates |
| Rotate webhook key | Not idempotent — each call generates a fresh key |
| Rotate API password | Not idempotent — each call generates a fresh password |
| Generate Lib Lite token | Not idempotent — each call creates a new active token |
| PayShap deactivate | Idempotent — repeat calls succeed silently |
| Transaction lookup | Idempotent (read-only) |
| Certificate export | Idempotent (read-only) |
| Bulk QR | Not idempotent — each call generates a fresh batch |
For credential-rotation endpoints, treat each call as if it will produce a new secret — don't retry on uncertain failure modes without confirming whether the rotation actually happened.
Common error responses
| HTTP | Typical body | Meaning |
|---|---|---|
200 OK | (operation result) | Success |
400 BAD_REQUEST | "Invalid 'fieldName' Field" | Validation failed on a request field |
400 BAD_REQUEST | "Invalid 'merchantId'" | Ownership check failed |
400 BAD_REQUEST | "Merchant not in 'ACTIVE' state" | State precondition failed |
401 UNAUTHORIZED | (empty) | Auth failed — bad credentials, missing ROLE_REMOTE, or wrong caller prefix for the endpoint |
500 INTERNAL_SERVER_ERROR | (varies) | Server error — raise a support ticket with the request body and timestamp |
Rate limits
The Portal API has no published per-request rate limits today. See Rate limits for the platform-wide position. Practical guidance:
- Don't poll — these are admin endpoints, not runtime endpoints. Querying transactions on a 1-second loop is anti-pattern; use the runtime Querying transactions for that.
- Batch where you can —
/restful/transactionsByDateis designed for batch pulls; use it rather than calling/restful/transactions/{txRef}in a loop. - Bulk QR is async — supply a
callbackUrlfor large batches rather than polling.
Versioning
The Portal API doesn't have a published version number today. Backward-incompatible changes go through the standard release-notes path — your tech lead will be notified.
For payload-version concerns (e.g. webhook payload V2 vs V3), that's a per-feature setting, not an API-version setting. See Webhooks for the webhook-version story.
What's next
- Endpoint reference index → Portal API overview
- Authentication setup → Authentication
- Each endpoint in detail → Merchant management · Notifications · API credentials · Lib Lite tokens · Transaction lookup · Transaction certificates · Bulk QR · PayShap management
- Support escalation → Support
Updated about 23 hours ago
