Accounts and exchange rates
Bills say what a customer was charged. Two more things make UniPay’s answers match your books between bill runs:
- Account balances — what each account owes right now, including what was paid at your own counters. Lookups then show processors today’s figure, and tell them when an account has closed.
- Exchange rates — your own rates, by day. UniPay uses them to show a bill in another currency and to settle a payment made in one.
The Axis Sync Engine publishes both from your billing system. This page is for integrating directly. Every call here needs the
unipay:bills scope.
Account balances
Section titled “Account balances”Publish a complete snapshot of your accounts in chunks, then tell UniPay the snapshot is finished:
flowchart LR
A[New sync generation] --> B["POST accounts<br/>up to 500 per call"]
B --> C{More accounts?}
C -- Yes --> B
C -- No --> D["POST finalize-account-sync"]
D --> E[Accounts not in this<br/>snapshot become stale]
Publish a chunk
Section titled “Publish a chunk”POST /api/app/service-provider/accounts
{ "syncGeneration": 20260930, "accounts": [ { "accountNumber": "100-2345", "name": "J. Moyo", "balance": 150.00, "currency": "USD" }, { "accountNumber": "100-2346", "name": "T. Dube", "balance": -20.00, "currency": "USD", "customerReference": "CUST-881" } ]}curl -X POST https://test.unipay.co.zw/api/app/service-provider/accounts \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d @accounts.json| Field | Required | Rules |
|---|---|---|
syncGeneration |
Yes | A number identifying this snapshot. Use a higher number for each new snapshot, and the same number on every chunk of it and on the finalize call. A timestamp such as 20260930 or 1759219200 works well. |
accounts |
Yes | 1 to 500 accounts. |
accountNumber |
Yes | As on your bills. Spaces, dashes, dots and case are ignored, as in lookups. |
currency |
Yes | Three-letter ISO 4217 code. An account with balances in two currencies is sent once per currency. |
balance |
Yes | Signed, like a bill’s amount: positive owes, zero settled, negative in credit. |
name |
The account holder. Left unchanged if you don’t send it. | |
customerReference |
Your own customer ID, stored as sent. Never shown to processors. |
Each account is added, or updated if UniPay already has that account number in that currency:
{ "inserted": 1, "updated": 1, "failed": 0, "errors": []}A row UniPay can’t store is counted in failed and listed in errors, with its accountNumber and one of these messages; the rest of
the chunk is stored.
message |
Cause |
|---|---|
Account number must contain letters or digits |
Nothing left after spaces, dashes and dots are removed. |
Currency is required |
Empty currency. |
An empty chunk, or more than 500 accounts, is refused with HTTP 400 and At least one account is required. or
Too many accounts for a synchronous bulk upsert; submit in smaller chunks. Sending a chunk again is safe: the same accounts are
updated to the same figures.
Finish the snapshot
Section titled “Finish the snapshot”When every chunk has been accepted, call
POST /api/app/service-provider/finalize-account-sync with the same
generation:
{ "syncGeneration": 20260930 }{ "successful": true, "message": "Reconciled; 12 account(s) marked stale." }Every account you published in an earlier snapshot but not in this one is marked stale: closed, transferred, or otherwise no
longer on your books. Calling it again is safe (0 account(s) marked stale).
Don’t finalize until the whole snapshot is in: accounts you haven’t sent yet would be marked stale too. If that happens, they come back as soon as you publish them.
What processors see
Section titled “What processors see”When a processor looks up an account:
amountDueNowis your published balance in the currency being quoted, less any payments UniPay has recorded since you published it. So a balance of 150.00, then a 13.69 payment through UniPay, shows 136.31 until your next snapshot.balanceAsAtis when you last published that balance, andaccountBalanceslists every currency you publish for the account.- A stale account looks up as status
3(Unknown) withsuccessful: false, and processors won’t take payment. When a bill still exists for it,balanceIsStale: truewarns them instead. - An account with no live bill still answers by its balance:
NotOwingfor zero,InCreditfor a credit.
Without account balances, lookups work out what’s owed from your bills and the payments made through UniPay alone. See Bills and accounts for how processors use these figures.
Publish a snapshot at least once a day — after your day-end is a good time — so payments made at your own counters show up quickly.
Exchange rates
Section titled “Exchange rates”Publish rates
Section titled “Publish rates”POST /api/app/service-provider/currency-rates
{ "rates": [ { "currencyCode": "USD", "baseCurrency": "USD", "rate": 1, "effectiveDate": "2026-09-30" }, { "currencyCode": "ZWG", "baseCurrency": "USD", "rate": 0.037, "effectiveDate": "2026-09-30" } ]}| Field | Rules |
|---|---|
currencyCode |
The currency the rate is for. |
baseCurrency |
Your base currency. |
rate |
How many units of baseCurrency one unit of currencyCode is worth. Above: 1 ZWG = 0.037 USD. Greater than zero. |
effectiveDate |
The day the rate applies from. |
- Send your base currency too, at rate
1, for every date. UniPay converts between any two currencies through the base, so it needs a row for both. - Send 1 to 1,000 rates per call. An empty list, or more than 1,000, is refused with HTTP 400.
- The answer is the number of rates stored, e.g.
2. A rate for a currency and date UniPay already has replaces it; rows with an empty code or a rate of zero or less are skipped and not counted. - Rates are kept by date and add up over time. Publish each day’s rates, every day.
A rate someone at your organisation keys by hand in the UniPay portal, under Exchange rates, wins for that day: your published rate for the same currency and date is skipped. Keying a rate by hand is done only in the portal, not through the API.
How rates are used
Section titled “How rates are used”| Where | Rate used |
|---|---|
| A processor asks for a bill in another currency | Your latest rate on or before today, up to 31 days old. The converted figure is marked indicative. |
| A payment in another currency settles a bill | Your own rate for the payment date — by default, one no older than the day before. Without one, the payment is recorded but not matched to a bill. |
With the rates above, a customer who pays ZWG 370.00 against a USD bill settles USD 13.69 of it. The payments feed shows both, with
allocationRate 0.037.
Read your rates
Section titled “Read your rates”GET /api/app/service-provider/currency-rates?days=30 lists
your rates from the last days days (1 to 366, default 30), newest first. Each row has source — 1 published through the API, 2
keyed in the portal — and, for keyed rates, who entered it.
Accepted currencies
Section titled “Accepted currencies”GET /api/app/service-provider/accepted-currencies returns the
currencies processors may take payment in for your bills — what they see as acceptedCurrencies:
["USD", "ZWG"]Axis sets the list up with you; ask your Axis contact to change it. Where none has been set, it’s every currency UniPay supports plus any you bill in or publish rates for.
UniPay API v1 · build e1eb5c49 · Powered by Axis Solutions