Skip to main content

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 firstAlways perform customer validation before processing payments to ensure the customer account is valid and active.

Endpoint

POST /isw/payments/pay
Reference: PaymentsController.java:25-29

Payment flow

1

Validate customer

First validate the customer account using the validation endpoint
2

Prepare payment request

Build the payment request with all required fields including amount, customer ID, and payment code
3

Key exchange

The service automatically performs key exchange to obtain authentication tokens
4

Encrypt sensitive data

PIN and OTP values are encrypted using the terminal session key
5

Generate signature

Authentication headers are generated with a signature of critical payment parameters
6

Submit payment

The payment request is submitted to the Phoenix API
7

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

requestReference
string
required
Unique identifier for this payment. Must be unique across all transactions.
amount
double
required
Transaction amount in the specified currency
customerId
string
required
Customer’s account number or identifier (validated in previous step)
paymentCode
long
required
Payment item code identifying the service or biller

Authentication fields

pin
string
Customer PIN for wallet payments. Encrypted before transmission.
otp
string
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

phoneNumber
string
Customer phone number for notifications
customerName
string
Full name of the customer
narration
string
Payment description or purpose
sourceOfFunds
string
Source of the payment (e.g., “Wallet”, “Bank Account”)
depositorName
string
Name of the person making the deposit
location
string
Transaction location or branch
currencyCode
string
Currency code (e.g., UGX, USD). Defaults to UGX.
cardData
object
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

cardData.pan
string
Primary Account Number (card number)
cardData.expiryDate
string
Card expiry date in YYMM format
cardData.track2
string
Track 2 data from card magnetic stripe
cardData.pinBlock
string
Encrypted PIN block
cardData.accountType
string
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 CodeMessageAction
90051INSUFFICIENT FUNDSCustomer has insufficient balance
90055WRONG PIN OR OTPPrompt user to re-enter PIN/OTP
90026DUPLICATE REQUEST REFERENCEUse a new unique request reference
90052ACCOUNT NOT FOUNDVerify customer ID
90098EXCEEDS CONFIGURED LIMITAmount 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 referencesEach 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

Build docs developers (and LLMs) love