Publish bills
Publish a bill with POST /api/app/service-provider/bill, scope unipay:bills.
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",...}public record Bill( string BillNo, // what processors look the bill up by: letters and digits only string UniqueNo, // your own internal reference string AccountNo, // lets customers pay by account string Name, string Description, DateOnly BillDate, DateOnly DueDate, string Currency, decimal Amount); // signed: negative means the account is in credit
// Status: "Created", "Updated" or "AlreadyExists" (sending the same bill twice is safe).public record PublishResult(bool Successful, string Message, string Status);
public static async Task<PublishResult> PublishBillAsync(UniPayClient client, Bill bill){ var result = await client.SendAsync<PublishResult>(HttpMethod.Post, "/api/app/service-provider/bill", bill); if (!result.Successful) throw new InvalidOperationException(result.Message); return result;}static JsonNode publishBill(UniPayClient client, String billNo, String accountNo, BigDecimal amount, String currency) throws IOException, InterruptedException { Map<String, Object> bill = new LinkedHashMap<>(); bill.put("billNo", billNo); // what processors look the bill up by: letters and digits only bill.put("uniqueNo", billNo); // your own internal reference bill.put("accountNo", accountNo); // lets customers pay by account bill.put("name", "Example Customer"); bill.put("description", "Rates - October 2026"); bill.put("billDate", LocalDate.now().toString()); bill.put("dueDate", LocalDate.now().plusDays(30).toString()); bill.put("currency", currency); bill.put("amount", amount); // signed: negative means the account is in credit
JsonNode result = client.send("POST", "/api/app/service-provider/bill", bill); if (!result.get("successful").asBoolean()) { throw new IllegalStateException(result.get("message").asText()); } return result; // status: "Created", "Updated" or "AlreadyExists" (sending the same bill twice is safe)}fun publishBill(client: UniPayClient, billNo: String, accountNo: String, amount: BigDecimal, currency: String): JsonNode { val bill = mapOf( "billNo" to billNo, // what processors look the bill up by: letters and digits only "uniqueNo" to billNo, // your own internal reference "accountNo" to accountNo, // lets customers pay by account "name" to "Example Customer", "description" to "Rates - October 2026", "billDate" to LocalDate.now().toString(), "dueDate" to LocalDate.now().plusDays(30).toString(), "currency" to currency, "amount" to amount, // signed: negative means the account is in credit ) val result = client.send("POST", "/api/app/service-provider/bill", bill) check(result["successful"].asBoolean()) { result["message"].asText() } return result // status: "Created", "Updated" or "AlreadyExists" (sending the same bill twice is safe)}def publish_bill(client: UniPayClient, bill_no: str, account_no: str, amount: Decimal, currency: str) -> dict: bill = { "billNo": bill_no, # what processors look the bill up by: letters and digits only "uniqueNo": bill_no, # your own internal reference "accountNo": account_no, # lets customers pay by account "name": "Example Customer", "description": "Rates - October 2026", "billDate": date.today().isoformat(), "dueDate": (date.today() + timedelta(days=30)).isoformat(), "currency": currency, "amount": float(amount), # signed: negative means the account is in credit } result = client.request("POST", "/api/app/service-provider/bill", json=bill) if not result["successful"]: raise RuntimeError(result["message"]) return result # status: "Created", "Updated" or "AlreadyExists" (sending the same bill twice is safe)async function publishBill(client: UniPayClient, billNo: string, accountNo: string, amount: number, currency: string) { const today = new Date(); const due = new Date(today.getTime() + 30 * 86_400_000); const bill = { billNo, // what processors look the bill up by: letters and digits only uniqueNo: billNo, // your own internal reference accountNo, // lets customers pay by account name: 'Example Customer', description: 'Rates - October 2026', billDate: today.toISOString().slice(0, 10), dueDate: due.toISOString().slice(0, 10), currency, amount, // signed: negative means the account is in credit }; const result = await client.request<{ successful: boolean; message: string; status: string }>( 'POST', '/api/app/service-provider/bill', bill, ); if (!result.successful) throw new Error(result.message); return result; // status: "Created", "Updated" or "AlreadyExists" (sending the same bill twice is safe)}type Bill struct { BillNo string `json:"billNo"` // what processors look the bill up by: letters and digits only UniqueNo string `json:"uniqueNo"` // your own internal reference AccountNo string `json:"accountNo"` // lets customers pay by account Name string `json:"name"` Description string `json:"description"` BillDate string `json:"billDate"` // yyyy-mm-dd DueDate string `json:"dueDate"` Currency string `json:"currency"` Amount json.Number `json:"amount"` // signed: negative means the account is in credit}
type PublishResult struct { Successful bool `json:"successful"` Message string `json:"message"` Status string `json:"status"` // "Created", "Updated" or "AlreadyExists" (sending the same bill twice is safe)}
func PublishBill(c *UniPayClient, bill Bill) (*PublishResult, error) { var result PublishResult if err := c.Do(http.MethodPost, "/api/app/service-provider/bill", bill, &result); err != nil { return nil, err } if !result.Successful { return nil, errors.New(result.Message) } return &result, nil}function publishBill(UniPayClient $client, string $billNo, string $accountNo, string $amount, string $currency): array{ $bill = [ 'billNo' => $billNo, // what processors look the bill up by: letters and digits only 'uniqueNo' => $billNo, // your own internal reference 'accountNo' => $accountNo, // lets customers pay by account 'name' => 'Example Customer', 'description' => 'Rates - October 2026', 'billDate' => date('Y-m-d'), 'dueDate' => date('Y-m-d', strtotime('+30 days')), 'currency' => $currency, 'amount' => (float) $amount, // signed: negative means the account is in credit ]; $result = $client->request('POST', '/api/app/service-provider/bill', $bill); if (!$result['successful']) { throw new RuntimeException($result['message']); } return $result; // status: "Created", "Updated" or "AlreadyExists" (sending the same bill twice is safe)}The bill
Section titled “The bill”{ "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”.
The answer
Section titled “The answer”{ "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"] } ] }}Many bills at once
Section titled “Many bills at once”| 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