Skip to main content

Authorize Transaction

Overview​

AuthorizeTransactionCommand signs a prepared transaction for the currently authenticated customer and then triggers its server-side execution. For PUSH_APPROVE, the customer can instead reject the request with RejectTransactionAuthorizationCommand; rejection never triggers transaction execution.

Only a non-expired transaction in Prepared or AwaitingAuthorisation state can be authorised. The transaction must belong to the authenticated tenant and customer profile.

Endpoint​

POST /api/bpm/selfservice/cmd

Authentication​

Customer authentication is required:

Authorization: Bearer <customer-access-token>
Content-Type: application/json

Mobile applications should also send their normal device and application headers:

X-App-Channel: Mobile
X-Device-Id: <stable-app-installation-id>
X-Client-Platform: Android
X-App-Version: <application-version>

Command names​

Approve: AuthorizeTransactionCommand
Reject: RejectTransactionAuthorizationCommand

Request parameters​

ParameterTypeRequiredDescription
transactionIdintegerYesid returned by GetPendingAuthorizationTransactionsQuery
authorizationCodestringConditionalPIN, OTP, or authenticator code required by the transaction's server-returned authorizationMethod

The frontend must not choose or override the authorisation method. Read authorizationMethod from Get Pending Authorization Transactions, render the corresponding UI, and submit only the required code.

authorizationMethod, tenantId, and userId are not accepted as authority in either decision payload. The backend uses the transaction's stored method and resolves ownership from the authenticated session.

Authorisation methods​

Server-returned methodFrontend inputauthorizationCode
PINTransaction PINRequired
OTP_EMAILOTP sent through the configured email flowRequired
OTP_MOBILEOTP sent through the configured mobile/SMS flowRequired
AUTHENTICATOR_APPCurrent authenticator application codeRequired
PUSH_APPROVEExplicit Approve action in the authenticated applicationOmit

Only methods enabled and fully configured by the deployment should be presented to customers.

Request examples​

PIN, OTP, or authenticator code​

{
"cmd": "AuthorizeTransactionCommand",
"data": "{\"transactionId\":123,\"authorizationCode\":\"<user-supplied-code>\"}"
}

PUSH_APPROVE: approve​

{
"cmd": "AuthorizeTransactionCommand",
"data": "{\"transactionId\":123}"
}

PUSH_APPROVE: reject​

{
"cmd": "RejectTransactionAuthorizationCommand",
"data": "{\"transactionId\":123,\"reason\":\"I did not initiate this transaction\"}"
}

reason is optional. The backend accepts the reject command only when the authenticated customer owns the transaction, it is still pending and unexpired, and its stored authorizationMethod is PUSH_APPROVE.

Example:

const commandData = { transactionId };

if (authorizationMethod !== 'PUSH_APPROVE') {
commandData.authorizationCode = authorizationCode;
}

const response = await fetch(
`${apiBaseUrl}/api/bpm/selfservice/cmd`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'X-App-Channel': 'Mobile',
'X-Device-Id': installationId,
'X-Client-Platform': platform,
'X-App-Version': appVersion,
},
body: JSON.stringify({
cmd: 'AuthorizeTransactionCommand',
data: JSON.stringify(commandData),
}),
},
);

const result = await response.json();

For rejection, send the same authenticated request with the rejection command:

await fetch(`${apiBaseUrl}/api/bpm/selfservice/cmd`, {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
cmd: 'RejectTransactionAuthorizationCommand',
data: JSON.stringify({ transactionId, reason }),
}),
});

Response​

The command marks the transaction as authorised and invokes the prepared transaction's execution command. Its successful outData therefore contains the execution result for the underlying transaction type rather than a separate fixed authorisation-only object.

The common response envelope is:

{
"ok": true,
"message": "Executed successfully",
"outData": {
"isSuccessful": true,
"statusCode": "00",
"message": "<transaction execution result>",
"data": {}
},
"logs": []
}

The fields inside outData.data depend on whether the prepared transaction is a transfer, airtime purchase, data purchase, bill payment, or another supported transaction type.

Clients must check both ok and outData.isSuccessful; HTTP 200 alone does not mean that authorisation and execution succeeded.

Invalid PIN and retry handling​

PIN failures use the customer's existing account-wide transaction-PIN counter. The current maximum is five failed validations. A successful validation resets the counter to zero; the fifth failure blocks the transaction PIN.

For a non-terminal invalid PIN, the command returns the error only to the requesting client:

{
"isSuccessful": false,
"statusCode": "439",
"message": "Your PIN is incorrect.",
"data": {
"transactionId": 123,
"authorizationStatus": "Prepared – Awaiting Signature",
"attemptsRemaining": 3,
"isTerminal": false,
"reasonCode": "INVALID_PIN"
}
}

No FCM, inbox, or SignalR notification is generated for an intermediate failed attempt. When the PIN becomes blocked, the pending transaction moves to terminal Authorisation Failed and the response uses isTerminal=true, attemptsRemaining=0, and reasonCode=PIN_BLOCKED. The backend then publishes one transaction_authorization_failed transaction update so another active client, such as the initiating web chat, can stop waiting.

OTP and authenticator-code validation are currently placeholders and must not be enabled for production until their validators and equivalent retry controls are implemented. PUSH_APPROVE has no credential-attempt counter because the authenticated mobile session and explicit decision are the current proof.

For a successful PUSH_APPROVE rejection, the common envelope contains:

{
"ok": true,
"message": "Executed successfully",
"outData": {
"isSuccessful": true,
"statusCode": "00",
"message": "Transaction rejected successfully.",
"data": {
"transactionId": 123,
"authorizationStatus": "Rejected"
}
},
"logs": []
}

State and idempotency​

The backend performs these checks before execution:

  1. Resolve the authenticated tenant and customer profile.
  2. Retrieve the transaction under that ownership scope.
  3. Confirm it is not expired or already authorised.
  4. Confirm it is in Prepared or AwaitingAuthorisation state.
  5. Validate the required signing input.
  6. Mark it authorised and trigger execution.

Disable both decision buttons after either submission and do not automatically retry the command. If the client receives an uncertain network result, refresh pending transactions and transaction status before deciding whether another user action is required.

Handled failure examples​

Transaction not owned by the profile​

{
"ok": true,
"message": "Executed successfully",
"outData": {
"isSuccessful": false,
"statusCode": "400",
"message": "Transaction was not found for the authenticated profile.",
"data": null
}
}

Expired transaction​

{
"ok": true,
"message": "Executed successfully",
"outData": {
"isSuccessful": false,
"statusCode": "400",
"message": "This transaction has expired. Please prepare a new transaction.",
"data": null
}
}

Other handled failures include:

  • transaction already authorised;
  • transaction in a state that cannot be authorised;
  • missing or invalid PIN;
  • missing required OTP/authenticator code;
  • unsupported authorisation method;
  • downstream transaction execution failure.

Missing or invalid customer authentication is rejected by the endpoint with HTTP 401.

Security requirements​

  • Never invoke this command merely because an FCM message was received or tapped.
  • Require a valid authenticated session and explicit user confirmation.
  • Obtain transactionId, transaction details, and authorizationMethod from the pending-transactions query after authentication.
  • Do not send tenantId, userId, source account ownership, or another profile identifier.
  • Never store or log authorizationCode.
  • Reject double taps in the UI and refresh authoritative state after ambiguous failures.
  • Do not allow a deep link or notification to choose the authorisation method.