Bills
The bills endpoints let your integration push supplier bills and business expenses into an organization’s books, attach the receipt or supplier invoice to each one, and approve them so they’re posted to the ledger.
Reading requires the bills:read scope; creating draft bills, attaching files
and adding notes require bills:write. Approving posts to the ledger, so
it needs its own scope, bills:approve, and the authorizing user’s role must
have the approve permission (Approve Documents — the built-in Admin,
Accountant and Standard roles have it; Purchase and Sales don’t). The user’s
role must also include purchase documents — a sales-only role cannot use
these endpoints. See Authentication.
Endpoints
Section titled “Endpoints”| Method | Path | Scope | Description |
|---|---|---|---|
GET | /api/v1/bills | bills:read | List bills (paginated). Filter by reference to find an existing bill. |
POST | /api/v1/bills | bills:write | Create a draft bill — or an approved one, with bills:approve as well. |
GET | /api/v1/bills/{id} | bills:read | Fetch a single bill with line items. |
POST | /api/v1/bills/{id}/approve | bills:approve | Approve (post) a draft or submitted bill. |
GET | /api/v1/bills/{id}/attachments | bills:read | List the files attached to a bill. |
POST | /api/v1/bills/{id}/attachments | bills:write | Attach receipts / supplier invoices to a bill. |
POST | /api/v1/bills/{id}/upload-links | bills:write | Get a 15-minute link someone can use to attach files to the bill without signing in. |
GET | /api/v1/bills/{id}/notes | bills:read | The notes in the bill’s History & Notes. |
POST | /api/v1/bills/{id}/notes | bills:write | Add a note to the bill’s History & Notes. |
Related lookups you will usually need: contacts (the supplier), the chart of accounts (the expense account) and tax rates. AI assistants can use all of this through the MCP server.
Importing expenses with receipts
Section titled “Importing expenses with receipts”Whether you are syncing from another system or letting an AI assistant work through a pile of receipts, the same sequence works well:
- Look up the reference data once.
GET /api/v1/chart-of-accounts(expense accounts havetypeExpense,Overhead, orDirectCosts) andGET /api/v1/tax-rates. Cache the ids. - Find or create the supplier.
GET /api/v1/contacts?type=supplier&search=City%20Taxi— if there’s no match,POST /api/v1/contactswith"contactType": "supplier". - Check you haven’t imported it already. Put the receipt or supplier
invoice number in
reference. Before creating, look for a bill with the same receipt number and supplier:GET /api/v1/bills?reference=RCPT-1001&contactId=…. IgnoreVOIDEDbills; if any other bill comes back, skip the receipt (or just attach the file). This makes a re-run or a retried request safe. - Create the bill —
POST /api/v1/bills(below). Leave it as a draft if a person should review it, or — with thebills:approvescope — send"status": "APPROVED"to post it straight away. - Attach the receipt —
POST /api/v1/bills/{id}/attachmentswith the PDF or photo. If you don’t hold the file yourself (an AI assistant in a chat), create an upload link and give it to whoever has the file. - Say where it came from — pass a
notewhen creating the bill. It lands in the bill’s History & Notes, for example “Total read from receipt photo; tax not shown”.
List bills
Section titled “List bills”Returns a paginated list of bills for the token’s organization, newest first. The list includes summary fields and the contact — not the line items.
Request
Section titled “Request”curl "https://app.accountfun.com/api/v1/bills?reference=RCPT-1001" \ -H "Authorization: Bearer $ACCESS_TOKEN"Query parameters
Section titled “Query parameters”| Parameter | Type | Default | Description |
|---|---|---|---|
status | string | — | Filter by status: DRAFT, SUBMITTED, APPROVED, PAID, or VOIDED. |
contactId | string | — | Filter to bills from a specific supplier. |
reference | string | — | Exact match on the bill’s reference, ignoring upper/lower case. _ and % match only themselves — there are no wildcards. Use it with contactId to check for an existing bill before creating one. |
page | integer | 1 | Page number (1-based, max 10000). |
limit | integer | 25 | Page size (max 200). |
Response — 200 OK
Section titled “Response — 200 OK”{ "bills": [ { "id": "7c0e5a1f-...", "billNumber": "BILL-0042", "reference": "RCPT-1001", "status": "DRAFT", "currency": "HKD", "total": "268.0000", "amountPaid": "0.0000", "balanceDue": "268.0000", "issueDate": "2026-09-15", "dueDate": "2026-09-15", "createdAt": "2026-10-07T03:12:44.120Z", "updatedAt": "2026-10-07T03:12:44.120Z", "contact": { "id": "...", "name": "City Taxi", "email": null } } ], "pagination": { "page": 1, "limit": 25, "total": 1, "totalPages": 1 }}Create a bill
Section titled “Create a bill”Creates a new bill. By default it’s a DRAFT — saved, but not yet posted
to the ledger. Add "status": "APPROVED" to create and approve it in one
atomic call; that needs the bills:approve scope as well as bills:write.
Request
Section titled “Request”curl -X POST https://app.accountfun.com/api/v1/bills \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "contactId": "00000000-0000-0000-0000-0000000000c1", "reference": "RCPT-1001", "issueDate": "2026-09-15", "dueDate": "2026-09-15", "taxMode": "inclusive", "lines": [ { "description": "Taxi — airport to client office", "quantity": "1", "unitAmount": "268.00", "accountId": "00000000-0000-0000-0000-0000000000a1" } ] }'Body parameters
Section titled “Body parameters”| Field | Type | Required | Description |
|---|---|---|---|
contactId | string | Yes | The supplier. Must be an active contact in the token’s organization. |
issueDate | date | Yes | The bill / receipt date, "YYYY-MM-DD". |
dueDate | date | Yes | "YYYY-MM-DD", on or after issueDate. For an already-paid receipt, use the same date. |
lines | array | Yes | 1–500 line items (see below). |
status | string | No | "DRAFT" (default) or "APPROVED" to post it in the same call. Approving needs the bills:approve scope and the approve permission. |
reference | string | No | The supplier’s invoice or receipt number. Max 200 characters. Recommended — it’s what GET /api/v1/bills?reference= matches on. |
currency | string | No | 3-letter ISO 4217 code. Defaults to the organization’s base currency. Must be enabled in the organization. |
exchangeRate | string | No | Rate to the base currency for a foreign-currency bill. Omit to use the day’s rate. |
billNumber | string | No | Your own bill number. If omitted, the next number in the organization’s sequence (e.g. BILL-0043) is used. Must be unique — a duplicate returns 409. Max 50 characters. |
taxMode | string | No | exclusive (amounts exclude tax), inclusive (amounts include tax), or none. Defaults to the organization’s purchases setting. |
projectId | string | No | Tag the bill to an open project. |
note | string | No | Added to the bill’s History & Notes in the same step, attributed to your app. Max 2000 characters. |
Dates may also be sent as an ISO 8601 datetime; the calendar date as
written is used ("2026-09-15T23:30:00-07:00" is September 15).
Line item parameters
Section titled “Line item parameters”| Field | Type | Required | Description |
|---|---|---|---|
quantity | string | Yes | Numeric string, >= 0, up to 4 decimal places. |
unitAmount | string | Yes | Numeric string, up to 4 decimal places; may be negative (e.g. a discount line). |
description | string | No | Max 500 characters. |
accountId | string | No* | The expense (or asset) account to post to. Required for non-zero lines. Must be active, and not one of the accounts a line can’t use. |
taxRateId | string | No | An active tax rate from GET /api/v1/tax-rates. Omit, null, or "" for no tax. |
itemId | string | No | Link the line to a product or service. A tracked item adds the quantity to stock when the bill is approved. |
discount | string | No | A positive flat amount ("5.00") or a percentage up to 100% ("10%"), with up to 4 decimal places. Max 30 characters. |
* A line whose total is zero may omit accountId.
All money and quantity values are strings ("268.00", not 268). A
single line can’t exceed 15 digits before the decimal point, and the bill’s
total (lines plus tax) can’t exceed 999,999,999,999,999.9999.
Accounts a line can’t use
Section titled “Accounts a line can’t use”Approving posts each line to its account, so a line can’t use an account that AccountFUN posts to itself, or one that money moves through:
- a bank account — record payments with Record Payment or Spend Money;
- the Accounts Receivable or Accounts Payable control account;
- the tax control account — choose a tax rate on the line instead;
- accumulated depreciation or accumulated amortisation;
- the unrealised currency gains and losses accounts;
- the inventory asset account, unless the line has an
itemId.
Expense, cost-of-sales, revenue, other asset, liability and equity accounts
are fine, and so is a tracked item’s inventory account. A line on one of the
accounts above returns 400 with a message naming the line and the account:
{ "error": "Line 2: \"Business Account\" is a bank account — record payments with Record Payment or Spend Money, not as a bill line"}Response — 201 Created
Section titled “Response — 201 Created”The created bill in the same shape as Fetch a single bill,
including its number, computed subtotal / taxTotal / total, and lines.
{ "bill": { "id": "7c0e5a1f-...", "billNumber": "BILL-0042", "reference": "RCPT-1001", "status": "DRAFT", "taxMode": "inclusive", "currency": "HKD", "subtotal": "268.0000", "taxTotal": "0.0000", "total": "268.0000", "journalEntryId": null, "...": "..." }}With "status": "APPROVED" the bill comes back APPROVED with the
journalEntryId of the posted entry (a zero-total bill is marked PAID).
Errors
Section titled “Errors”| Status | Cause |
|---|---|
400 | Invalid body — the details object names each bad field (including any id that isn’t a valid id, an invalid discount, or a total over 999,999,999,999,999.9999). |
400 | The contact is deactivated, an account is archived, a tax rate is inactive or not in the organization, the currency isn’t enabled, or the project is closed. |
400 | A line uses an account it can’t — the message names the line and the account. See Accounts a line can’t use. |
403 | The contact, an account, or an item does not belong to this organization. |
403 | The issue date falls on or before the organization’s lock date. |
403 | Insufficient role — creating needs create + purchase documents. |
403 | "status": "APPROVED" without the bills:approve scope or the approve permission. The message names bills:approve; nothing is created — send it as a draft instead. |
409 | Duplicate billNumber, or a bill rule was violated (for example a negative total — use a supplier credit note instead). |
Fetch a single bill
Section titled “Fetch a single bill”curl https://app.accountfun.com/api/v1/bills/7c0e5a1f-... \ -H "Authorization: Bearer $ACCESS_TOKEN"Response — 200 OK
Section titled “Response — 200 OK”{ "bill": { "id": "7c0e5a1f-...", "contactId": "...", "billNumber": "BILL-0042", "reference": "RCPT-1001", "status": "APPROVED", "taxMode": "inclusive", "currency": "HKD", "exchangeRate": null, "subtotal": "268.0000", "taxTotal": "0.0000", "total": "268.0000", "amountPaid": "0.0000", "balanceDue": "268.0000", "issueDate": "2026-09-15", "dueDate": "2026-09-15", "postedAt": "2026-10-07T03:15:02.511Z", "voidedAt": null, "journalEntryId": "...", "projectId": null, "createdAt": "2026-10-07T03:12:44.120Z", "updatedAt": "2026-10-07T03:15:02.511Z", "contact": { "id": "...", "name": "City Taxi", "email": null }, "lines": [ { "id": "...", "description": "Taxi — airport to client office", "quantity": "1.0000", "unitAmount": "268.0000", "discount": null, "amount": "268.0000", "taxAmount": "0.0000", "accountId": "...", "taxRateId": null, "itemId": null, "trackingOption1Id": null, "trackingOption2Id": null, "account": { "id": "...", "code": "493", "name": "Travel - National", "type": "Expense" }, "taxRate": null } ] }}Business dates (issueDate, dueDate) are "YYYY-MM-DD"; timestamps
(postedAt, createdAt, …) are UTC ISO instants. A bill in another
organization — or an id that isn’t a valid id — returns 404.
Approve a bill
Section titled “Approve a bill”Approve (post) a DRAFT or SUBMITTED bill — the same action as clicking
Approve inside AccountFUN. Requires the bills:approve scope and the
approve permission. No body.
curl -X POST https://app.accountfun.com/api/v1/bills/7c0e5a1f-.../approve \ -H "Authorization: Bearer $ACCESS_TOKEN"Response — 200 OK
Section titled “Response — 200 OK”{ "billId": "7c0e5a1f-...", "status": "APPROVED", "journalEntryId": "..." }status is the bill’s status after posting — a zero-total bill goes straight
to PAID with journalEntryId: null.
Errors
Section titled “Errors”| Status | Cause |
|---|---|
400 | The bill can’t be approved in its current state (already approved, paid, or voided). |
403 | insufficient_scope — the token doesn’t carry bills:approve (bills:write and invoices:approve don’t approve bills), or the user’s role doesn’t have the approve permission. |
403 | The issue date falls on or before the lock date. |
404 | No bill with that id in the token’s organization. |
409 | A posting rule failed (for example no exchange rate for the bill date), or the organization’s Accounts Payable / tax system accounts are missing. |
Attach receipts
Section titled “Attach receipts”Attach one or more files to a bill — at any status, so a receipt can follow an approved bill.
- File types: PDF, PNG, JPEG, XLSX, XLS, DOCX, DOC, CSV. For PDF, image
and Office files the content must match the extension: an XLS or DOC file
must be a genuine Office file, and an XLSX or DOCX file a genuine Excel or
Word file (a ZIP or a macro-enabled
.xlsmrenamed to.xlsxis refused). - Limits: 5 MB per file, 25 MB per request, 10 attachments per bill, and the organization’s storage quota.
- Attachments follow the shared file processing rules: documents are antivirus-checked and can remain pending until cleared; images are resized and re-encoded as JPEG with metadata removed.
Multipart upload
Section titled “Multipart upload”Send multipart/form-data with one or more file parts:
curl -X POST https://app.accountfun.com/api/v1/bills/7c0e5a1f-.../attachments \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -F "file=@receipt-1001.pdf"JSON upload (base64)
Section titled “JSON upload (base64)”If your tool can only send JSON — common for low-code automation — send the file as base64 instead:
curl -X POST https://app.accountfun.com/api/v1/bills/7c0e5a1f-.../attachments \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"files\": [{\"fileName\": \"receipt-1001.jpg\", \"contentBase64\": \"$(base64 -w0 receipt-1001.jpg)\"}]}"| Field | Type | Description |
|---|---|---|
files[].fileName | string | File name with its extension — the extension decides the file type. |
files[].contentBase64 | string | The file’s bytes, base64-encoded. A data:…;base64, prefix is accepted. |
Response — 201 Created
Section titled “Response — 201 Created”{ "attachments": [ { "id": "...", "originalName": "receipt-1001.pdf", "mimeType": "application/pdf", "fileSize": 48213, "createdAt": "2026-10-07T03:13:10.004Z" } ]}When you send several files and some are rejected (wrong type, too large),
the stored ones are returned with a warnings array explaining the others.
Errors
Section titled “Errors”| Status | Cause |
|---|---|
400 | No file, invalid base64, every file was rejected, or the bill already has 10 attachments. |
403 | Insufficient role (view-only roles can’t upload). |
404 | No bill with that id in the token’s organization. |
413 | The request is over 25 MB, or the organization’s storage quota is full. |
503 | Virus scanning is temporarily unavailable — retry later. Nothing unscanned is stored. |
Upload links
Section titled “Upload links”When the caller can’t send the file itself, it can ask for a short-lived link and hand it to whoever has the file. An AI assistant in a chat uses this: it has seen the receipt but doesn’t hold the original file, so it gives the user a link to attach it.
curl -X POST https://app.accountfun.com/api/v1/bills/7c0e5a1f-.../upload-links \ -H "Authorization: Bearer $ACCESS_TOKEN"No body. Response — 201 Created:
{ "uploadLink": { "url": "https://app.accountfun.com/upload/9f2c…", "uploadUrl": "https://app.accountfun.com/upload/9f2c…/files", "expiresAt": "2026-10-07T10:15:00.000Z", "maxFiles": 5 }}-
urlopens a page, no sign-in needed, where a person picks the files or takes a photo on their phone. The page shows only the bill number. Photos over 5 MB are scaled down on the phone before they’re sent. -
uploadUrltakes the files directly, asmultipart/form-data, with noAuthorizationheader: the link is the authorization.Terminal window curl -F "file=@receipt-1001.jpg" https://app.accountfun.com/upload/9f2c…/filesIt answers
201with{ attachments, warnings?, filesRemaining },404for an unknown link,410once the link has expired, has taken all its files, or its app was disconnected, and403if the user who created it can no longer change the bill.
A link:
- works for 15 minutes and takes up to 5 files in total. Rejected files don’t count;
- can only add files to this one bill. It can’t show, list or change anything;
- stops working when the app is disconnected or loses
bills:write, or when the user who created it leaves the organization or can no longer change bills; - is stored only as a hash. Ask for a new link each time rather than keeping one.
Uploads follow the same rules as attaching receipts: file types, the 5 MB and 10-per-bill limits, the storage quota and the virus scan. In the bill’s History & Notes, creating the link shows as Upload Link Created and each file as File uploaded through an upload link, both under the user’s name via the app.
List attachments
Section titled “List attachments”curl https://app.accountfun.com/api/v1/bills/7c0e5a1f-.../attachments \ -H "Authorization: Bearer $ACCESS_TOKEN"Returns { "attachments": [ … ] } with each file’s id, originalName,
mimeType, fileSize, scanStatus, and createdAt, oldest first.
Read and add notes in the bill’s History & Notes:
curl -X POST https://app.accountfun.com/api/v1/bills/7c0e5a1f-.../notes \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{"body": "Receipt total includes a 10% service charge, recorded on one line."}'201 returns { "note": { "id", "body", "authorId", "viaApp", "createdAt" } }.
GET lists the notes newest first. viaApp names the app that wrote a note,
or is null for a note typed in AccountFUN or restored from a data export.
Notes are up to 2000 characters. View-only roles can’t add them.
History and audit trail
Section titled “History and audit trail”Everything your integration changes is recorded as the user who authorized it, via your app, with the time and where the request came from (IP address and user agent). Your app’s name is the one registered under API & Developers. This covers:
- bills created and approved — the ledger posting of an approved bill names your app too;
- files uploaded or attached;
- notes added.
These entries appear in the bill’s History & Notes (shown as “Jane Lee — via Your App (API)”) and in the organization’s Audit History. Every API request is also logged, and the logs are kept for 30 days.
Bill statuses
Section titled “Bill statuses”| Status | Meaning |
|---|---|
DRAFT | Saved, not posted. Editable in AccountFUN. |
SUBMITTED | Awaiting approval. |
APPROVED | Posted — the expense and what you owe are in the books. |
PAID | Fully paid (balanceDue is zero). |
VOIDED | Cancelled and reversed out of the ledger. |
Over the API you can create drafts, create-and-approve, and approve. Editing, deleting, voiding, and recording payments are done inside AccountFUN.