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

RuleDetail
TLS 1.2+ mandatoryHTTP requests are rejected at the load balancer. No exceptions.
Basic Auth on every requestAuthorization: Basic <base64(username:password)> — no session reuse, no cookies, no token caching server-side.
ROLE_REMOTE opt-in requiredThe credentialled profile must have ROLE_REMOTE granted, or every API call returns 401. Granted by Scan to Pay Operations on request.
Caller-prefix routingUsername's prefix (PSP_, ACQUIRER_, MERCHANT_) determines what endpoints you can hit. See Authentication.
No session stateEach 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.

CallerOwnership rule
PSPTarget merchant's pspId must match the caller's PSP ID
AcquirerTarget merchant's acquirer must match the caller's acquirer name
MerchantTarget 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:

OperationRequired state
UpdateACTIVE
SuspendACTIVE
UnsuspendSUSPENDED
Add / update notification (HTTP, Email, SMS)ACTIVE
Rotate webhook keyACTIVE
Rotate API passwordACTIVE
List notificationsACTIVE
Generate Lib Lite tokenACTIVE
List Lib Lite tokensACTIVE

Endpoints that don't enforce state:

OperationWhy no state check
List merchantsRead-only across all states in your scope
CreateNo existing merchant to check (creates the row)
Deactivate PayShapUseful to clean up suspended merchants before termination
Transaction lookupHistorical data should be accessible regardless of current state
Transaction certificatesAudit 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

RuleDetail
Content-Type: application/json requiredOther content types are rejected with HTTP 400.
JSON request bodiesValidated 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 IDsSome 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

RuleDetail
JSON responses by defaultExcept for the certificate endpoints, which return a String body containing a base64-encoded PDF.
HTTP 200 OK for successIncluding no-op idempotent successes (e.g. PayShap deactivate on a merchant that wasn't using PayShap).
HTTP 200 OK with empty arrayFor list endpoints with no matching records. Not 404.
Plain-text error bodiesMost 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:

FieldWhat's logged
Acting userThe authenticated principal (PSP, acquirer, or merchant identity)
SourcePORTAL_API_PSP, PORTAL_API_ACQUIRER, or PORTAL_API_MERCHANT — distinguishes API calls from Portal UI actions
Action typeThe specific operation (e.g. ACTION_MERCHANT_UPDATE, ACTION_NOTIFY_UPDATE, ACTION_MERCHANT_API_PASSWORD_GENERATE)
TargetThe merchant ID or other entity being acted on
Reason / detailFree-text where applicable (e.g. suspend / unsuspend reasons)
TimestampUTC, 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:

OperationIdempotency model
Create merchantNot idempotent — repeat calls create duplicate merchant records. Use unique data per call.
Update merchantIdempotent — same input, same result
Suspend / UnsuspendIdempotent 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 keyNot idempotent — each call generates a fresh key
Rotate API passwordNot idempotent — each call generates a fresh password
Generate Lib Lite tokenNot idempotent — each call creates a new active token
PayShap deactivateIdempotent — repeat calls succeed silently
Transaction lookupIdempotent (read-only)
Certificate exportIdempotent (read-only)
Bulk QRNot 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

HTTPTypical bodyMeaning
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/transactionsByDate is designed for batch pulls; use it rather than calling /restful/transactions/{txRef} in a loop.
  • Bulk QR is async — supply a callbackUrl for 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