Overview
The payment endpoint allows you to process transactions after successfully validating customer details. It supports multiple payment methods including wallet payments, card payments, and PIN/OTP-based authentication.
Validate customers first Always perform customer validation before processing payments to ensure the customer account is valid and active.
Endpoint
Reference: PaymentsController.java:25-29
Payment flow
Validate customer
First validate the customer account using the validation endpoint
Prepare payment request
Build the payment request with all required fields including amount, customer ID, and payment code
Key exchange
The service automatically performs key exchange to obtain authentication tokens
Encrypt sensitive data
PIN and OTP values are encrypted using the terminal session key
Generate signature
Authentication headers are generated with a signature of critical payment parameters
Submit payment
The payment request is submitted to the Phoenix API
Check status
Use the request reference to check transaction status
Request body
{
"requestReference" : "038938738738" ,
"amount" : 100.00 ,
"customerId" : "12345" ,
"phoneNumber" : "987-654-3210" ,
"paymentCode" : 56789 ,
"customerName" : "Alice Smith" ,
"sourceOfFunds" : "Bank Account" ,
"narration" : "Payment for order" ,
"depositorName" : "Bob Johnson" ,
"location" : "City XYZ" ,
"alternateCustomerId" : "54321" ,
"pin" : "1234" ,
"otp" : "567890" ,
"currencyCode" : "UGX"
}
Reference: Phoenix API Sample.postman_collection.json:113-115
Required fields
Unique identifier for this payment. Must be unique across all transactions.
Transaction amount in the specified currency
Customer’s account number or identifier (validated in previous step)
Payment item code identifying the service or biller
Authentication fields
Customer PIN for wallet payments. Encrypted before transmission.
One-time password if required by the biller. Encrypted before transmission.
PIN and OTP values are automatically encrypted using AES-256-CBC encryption with the terminal session key before being sent to the API.
Optional fields
Customer phone number for notifications
Full name of the customer
Payment description or purpose
Source of the payment (e.g., “Wallet”, “Bank Account”)
Name of the person making the deposit
Transaction location or branch
Currency code (e.g., UGX, USD). Defaults to UGX.
Card payment details if paying by card. See card payment section below.
Implementation
The payment processing logic is implemented in PaymentsService.makePayment():
public String makePayment ( PaymentRequest request) throws Exception {
String endpointUrl = Constants . ROOT_LINK + "sente/xpayment" ;
request . setTerminalId ( Constants . TERMINAL_ID );
// Build additional data for signature
String additionalData = request . getAmount () + "&"
+ request . getTerminalId () + "&"
+ request . getRequestReference () + "&"
+ request . getCustomerId () + "&"
+ request . getPaymentCode ();
SystemResponse < KeyExchangeResponse > exchangeKeys = keyExchangeService . doKeyExchange ();
if ( exchangeKeys . getResponseCode (). equals ( PhoenixResponseCodes . APPROVED . CODE )) {
String authToken = exchangeKeys . getResponse (). getAuthToken ();
String sessionKey = exchangeKeys . getResponse (). getTerminalKey ();
// Encrypt OTP if provided
if ( request . getOtp () != null )
request . setOtp ( CryptoUtils . encrypt (
request . getOtp (),
exchangeKeys . getResponse (). getTerminalKey ()
));
Map < String , String > headers = AuthUtils . generateInterswitchAuth (
Constants . POST_REQUEST ,
endpointUrl,
additionalData,
authToken,
sessionKey
);
return HttpUtil . postHTTPRequest (endpointUrl, headers,
JSONDataTransform . marshall (request));
}
else {
return JSONDataTransform . marshall (exchangeKeys);
}
}
Reference: PaymentsService.java:42-72
Security features
OTP encryption
When an OTP is provided, it’s encrypted using the terminal session key:
if ( request . getOtp () != null )
request . setOtp ( CryptoUtils . encrypt (
request . getOtp (),
exchangeKeys . getResponse (). getTerminalKey ()
));
Reference: PaymentsService.java:60-61
Signature generation
Critical payment parameters are included in the signature to prevent tampering:
String additionalData = request . getAmount () + "&"
+ request . getTerminalId () + "&"
+ request . getRequestReference () + "&"
+ request . getCustomerId () + "&"
+ request . getPaymentCode ();
Reference: PaymentsService.java:47-50
The signature is generated using SHA-256 RSA signing with your private key.
Card payments
For card-based payments, include the cardData object:
{
"amount" : 100.00 ,
"customerId" : "12345" ,
"paymentCode" : 56789 ,
"cardData" : {
"pan" : "5060990580000217499" ,
"expiryDate" : "2512" ,
"track2" : "5060990580000217499=2512101123456789" ,
"pinBlock" : "encrypted_pin" ,
"accountType" : "SAVINGS" ,
"posEntryMode" : "051" ,
"posDataCode" : "510101511344101"
}
}
Card data fields
Primary Account Number (card number)
Card expiry date in YYMM format
Track 2 data from card magnetic stripe
Account type (SAVINGS, CURRENT, etc.)
Reference: CardData.java:5-26
Response handling
Success response
{
"responseCode" : "90000" ,
"responseMessage" : "TRANSACTION APPROVED" ,
"requestReference" : "038938738738" ,
"transactionReference" : "ISW20240315123456" ,
"amount" : 100.00 ,
"customerId" : "12345"
}
Save the transactionReference from the response. Use it to query transaction status or for reconciliation.
Pending response
Some transactions may be pending and require status checks:
{
"responseCode" : "90009" ,
"responseMessage" : "REQUEST IN PROGRESS" ,
"requestReference" : "038938738738"
}
For pending transactions (code 90009), use the transaction status endpoint to check the final status.
Error responses
Response Code Message Action 90051 INSUFFICIENT FUNDS Customer has insufficient balance 90055 WRONG PIN OR OTP Prompt user to re-enter PIN/OTP 90026 DUPLICATE REQUEST REFERENCE Use a new unique request reference 90052 ACCOUNT NOT FOUND Verify customer ID 90098 EXCEEDS CONFIGURED LIMIT Amount exceeds transaction limit
Reference: PhoenixResponseCodes.java:28-32
Code examples
Basic payment
@ Autowired
private PaymentsService paymentsService ;
public String processPayment ( String customerId, double amount, long paymentCode) {
try {
PaymentRequest request = new PaymentRequest ();
request . setRequestReference ( UUID . randomUUID (). toString ());
request . setCustomerId (customerId);
request . setPaymentCode (paymentCode);
request . setAmount (amount);
request . setPhoneNumber ( "256700000000" );
request . setCurrencyCode ( "UGX" );
request . setNarration ( "Bill payment" );
String response = paymentsService . makePayment (request);
return response;
} catch ( Exception e ) {
System . err . println ( "Payment failed: " + e . getMessage ());
throw new PaymentException (e);
}
}
Payment with OTP
public String processPaymentWithOTP ( PaymentRequest request, String otp) {
try {
request . setRequestReference ( UUID . randomUUID (). toString ());
request . setOtp (otp); // Will be encrypted automatically
String response = paymentsService . makePayment (request);
return response;
} catch ( Exception e ) {
System . err . println ( "Payment with OTP failed: " + e . getMessage ());
throw new PaymentException (e);
}
}
Using the REST endpoint
curl -X POST http://localhost:8081/isw/payments/pay \
-H "Content-Type: application/json" \
-d '{
"requestReference": "038938738738",
"amount": 100.00,
"customerId": "12345",
"phoneNumber": "256700000000",
"paymentCode": 56789,
"customerName": "Alice Smith",
"currencyCode": "UGX",
"narration": "Bill payment"
}'
Reference: README.md:12
Best practices
Never reuse request references Each payment must have a unique requestReference. Reusing references will result in duplicate transaction errors.
Idempotency
The Phoenix API uses request references for idempotency. If you retry a payment with the same reference, you’ll receive the original response without processing a duplicate payment.
Amount validation
Validate amounts before submitting:
if (amount <= 0 ) {
throw new IllegalArgumentException ( "Amount must be positive" );
}
if (amount > MAX_TRANSACTION_LIMIT) {
throw new IllegalArgumentException ( "Amount exceeds maximum limit" );
}
Response parsing
Parse and validate the response before marking transactions as successful:
JSONObject response = new JSONObject (responseString);
String responseCode = response . getString ( "responseCode" );
if ( "90000" . equals (responseCode)) {
String transactionRef = response . getString ( "transactionReference" );
// Save transaction reference
return transactionRef;
} else if ( "90009" . equals (responseCode)) {
// Pending - check status later
scheduleStatusCheck (requestReference);
} else {
String errorMessage = response . getString ( "responseMessage" );
throw new PaymentException (errorMessage);
}
Error recovery
Implement retry logic for transient errors:
List < String > retryableCodes = Arrays . asList ( "90091" , "90096" );
if ( retryableCodes . contains (responseCode)) {
// Retry with exponential backoff
Thread . sleep ( 5000 );
return retryPayment (request);
}
Next steps
Check transaction status Monitor payment status using request references
Wallet balance Check available wallet balance before payments