Skip to content

Pay a bill

Once you have taken the customer’s money, report it with POST /api/app/payment-processor/payment-details, scope unipay:bills. The examples retry safely until the outcome is known.

Terminal window
CURRENCY=$(echo "$LOOKUP" | jq -r .bill.currency)
RRN="EX$(date +%s%3N)" # your unique reference: reuse it on every retry of this payment
curl -sS --fail-with-body -X POST "$BASE_URL/api/app/payment-processor/payment-details" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"billNo\": \"$BILL_NO\",
\"rrn\": \"$RRN\",
\"paymentDate\": \"$(date +%Y-%m-%dT%H:%M:%S%:z)\",
\"currency\": \"$CURRENCY\",
\"amount\": $AMOUNT
}"
# → {"successful":true,"message":"Payment Saved","isNewRecord":true,...}
{
"billNo": "4444INV0009",
"rrn": "PX20260929000123",
"paymentDate": "2026-09-29T10:15:00+02:00",
"currency": "USD",
"amount": 170.00
}

To pay against an account instead, replace billNo with serviceProvider and accountNumber, as in the lookup. The payment lands on the same bill the lookup returned.

Field Required Rules
billNo Yes, or accountNumber The bill being paid, from the lookup. If you send both, the bill number wins.
serviceProvider With accountNumber Provider number or short code.
accountNumber Or billNo Up to 50 characters.
rrn Yes Your unique reference for this payment, up to 50 characters. See Idempotency.
paymentDate Yes When the customer paid. Include a UTC offset (+02:00 or Z) — a time without one is read as UTC. Not in the future and not more than 90 days ago, judged by the calendar day in Zimbabwe.
currency Yes Three-letter ISO 4217 code: the bill’s currency or one of its acceptedCurrencies.
amount Yes What the customer paid, greater than zero.

Paying more than is owed, and paying after the due date, are both accepted — unless the service provider has chosen to refuse overpayments.

{ "successful": true, "message": "Payment Saved", "isNewRecord": true }

"isNewRecord": false with successful: true means UniPay already had this rrn — the payment is recorded; treat it as success. The message is then Duplicate notification; already processed.

A payment UniPay can’t accept answers HTTP 200 with "successful": false. Nothing is recorded: reverse the customer’s debit on your side and show the reason.

message Cause
Bill '4444INV0009' not found. Cannot process payment. No bill with that number.
No bill found for that account. Cannot process payment. Account payment: the account has no bill.
Currency 'EUR' is not accepted by this service provider. Accepted currencies: USD, ZWG Pay in an accepted currency.
Invoice has expired The bill’s expiryDate has passed.
Payment would exceed the remaining balance on this invoice Overpayment, where the provider refuses them.
Payment date cannot be in the future Check your clock and the offset on paymentDate.
Payment date cannot be more than 90 days in the past Too late to report through the API; contact support.
Service provider is required when paying by account number Add serviceProvider.
RRN 'X' was already used for a different payment You sent an rrn you’d already used, for a different bill, account, amount or currency. Give each payment its own rrn.
Retrieval Reference Number is required · RRN must not exceed 50 characters Fix rrn.
Payment amount must be greater than zero · Currency must be 3 characters · Payment date is required Fix the field.

A request the API can’t parse at all gets HTTP 400 — see Errors and messages.

This is the part to get right. If a call times out, or you get a 429 or 5xx, you don’t know whether the payment was recorded. Send the same request again with the same rrn: if it was recorded you get isNewRecord: false, and nothing is recorded twice.

An rrn is yours alone: another processor using the same value doesn’t affect you. Never reuse one of your own for a different payment — UniPay refuses it — and never reverse a customer’s debit because a report timed out. The whole policy is in Idempotency and retries.

UniPay API v1 · build e1eb5c49 · Powered by Axis Solutions