Skip to content

Publish bills

Publish a bill with POST /api/app/service-provider/bill, scope unipay:bills.

Terminal window
curl -sS --fail-with-body -X POST "$BASE_URL/api/app/service-provider/bill" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"billNo\": \"$BILL_NO\",
\"uniqueNo\": \"$BILL_NO\",
\"accountNo\": \"$ACCOUNT_NO\",
\"name\": \"Example Customer\",
\"description\": \"Rates - October 2026\",
\"billDate\": \"$(date +%Y-%m-%d)\",
\"dueDate\": \"$(date -d '+30 days' +%Y-%m-%d)\",
\"currency\": \"$CURRENCY\",
\"amount\": $AMOUNT
}"
# → {"successful":true,"message":"Added","isNewRecord":true,"status":"Created",...}
{
"billNo": "4444INV0009",
"uniqueNo": "INV0009",
"accountNo": "100-2345",
"name": "J. Moyo",
"description": "Rates - September 2026",
"billDate": "2026-09-01",
"dueDate": "2026-09-30",
"currency": "USD",
"amount": 220.00,
"expiryDate": null
}
Field Required Rules
billNo Yes What processors look the bill up by. 4–50 letters and digits only — no spaces or dashes. Unique within your organisation per currency; prefixing your provider number works well (4444INV0009).
uniqueNo Yes Your own internal reference: invoice, statement or case number. Needn’t be unique.
accountNo Recommended The customer’s account. Without it the bill can’t be paid by account.
name The account holder, as the customer will recognise it.
description Yes What the bill is for.
billDate, dueDate Yes Dates. Paying after dueDate is still accepted.
expiryDate After this date the bill can’t be paid.
currency Yes Three-letter ISO 4217 code: the bill’s native currency.
amount Yes Signed. Positive: the account owes. Zero: settled. Negative: in credit.

Your organisation comes from your token; there’s no provider field to send.

Publish a bill for every account, whatever its balance — zero and credit balances included. That’s what lets any customer pay any account, and lets a lookup say “nothing owing” instead of “not found”.

{ "successful": true, "message": "Added", "isNewRecord": true, "status": "Created" }
status Meaning
Created A new bill.
Updated The bill existed and has been updated with what you sent.
AlreadyExists The bill existed, unchanged. Nothing to do — publishing again is safe.
Blocked successful: false. The bill already has payments against it, so it can’t be changed automatically (errorCode: bill_has_payments). Raise a new bill instead.

A malformed bill — a billNo with a dash, a missing description — is rejected with HTTP 400 listing each problem:

{
"error": {
"message": "Your request is not valid!",
"validationErrors": [
{ "message": "Bill number can only contain letters and numbers", "members": ["billNo"] }
]
}
}
Volume Endpoint How
Up to 500 POST /api/app/service-provider/bills Synchronous. Send {"bills": [ … ]}; the answer has a result per bill.
Up to 5,000 POST /api/app/bill-import/submit Asynchronous. Send {"bills": [ … ], "idempotencyKey": "…"}; you get a batchId back.

A batch answers with a count and one result per bill; each result’s status is a number here — 1 Created, 2 AlreadyExists, 3 Updated, 4 Blocked, 5 Failed:

{
"total": 2, "succeeded": 2, "failed": 0,
"results": [
{ "billNo": "4444INV0009", "successful": true, "message": "Added", "status": 1 },
{ "billNo": "4444INV0010", "successful": true, "message": "Bill number '4444INV0010' already exists for currency USD.", "status": 2 }
]
}

For an import, give each submission its own idempotencyKey (up to 64 characters) and reuse it if you retry the submit. Then poll GET /api/app/bill-import/status/{batchId} until status is 2 (Completed), 3 (CompletedWithErrors) or 4 (Failed) — 0 is Pending and 1 Processing — and fetch the failed rows from GET /api/app/bill-import/errors/{batchId}.

UniPay API v1 · build e1eb5c49 · Powered by Axis Solutions