Skip to content

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.

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]

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" }
]
}
Terminal window
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.

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.

When a processor looks up an account:

  • amountDueNow is 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.
  • balanceAsAt is when you last published that balance, and accountBalances lists every currency you publish for the account.
  • A stale account looks up as status 3 (Unknown) with successful: false, and processors won’t take payment. When a bill still exists for it, balanceIsStale: true warns them instead.
  • An account with no live bill still answers by its balance: NotOwing for zero, InCredit for 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.

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.

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.

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.

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