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​
| Parameter | Type | Required | Description |
|---|---|---|---|
transactionId | integer | Yes | id returned by GetPendingAuthorizationTransactionsQuery |
authorizationCode | string | Conditional | PIN, 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 method | Frontend input | authorizationCode |
|---|---|---|
PIN | Transaction PIN | Required |
OTP_EMAIL | OTP sent through the configured email flow | Required |
OTP_MOBILE | OTP sent through the configured mobile/SMS flow | Required |
AUTHENTICATOR_APP | Current authenticator application code | Required |
PUSH_APPROVE | Explicit Approve action in the authenticated application | Omit |
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:
- Resolve the authenticated tenant and customer profile.
- Retrieve the transaction under that ownership scope.
- Confirm it is not expired or already authorised.
- Confirm it is in
PreparedorAwaitingAuthorisationstate. - Validate the required signing input.
- 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, andauthorizationMethodfrom 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.