Skip to content
AccountFUNDocs

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.

MethodPathScopeDescription
GET/api/v1/billsbills:readList bills (paginated). Filter by reference to find an existing bill.
POST/api/v1/billsbills:writeCreate a draft bill — or an approved one, with bills:approve as well.
GET/api/v1/bills/{id}bills:readFetch a single bill with line items.
POST/api/v1/bills/{id}/approvebills:approveApprove (post) a draft or submitted bill.
GET/api/v1/bills/{id}/attachmentsbills:readList the files attached to a bill.
POST/api/v1/bills/{id}/attachmentsbills:writeAttach receipts / supplier invoices to a bill.
POST/api/v1/bills/{id}/upload-linksbills:writeGet a 15-minute link someone can use to attach files to the bill without signing in.
GET/api/v1/bills/{id}/notesbills:readThe notes in the bill’s History & Notes.
POST/api/v1/bills/{id}/notesbills:writeAdd 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.


Whether you are syncing from another system or letting an AI assistant work through a pile of receipts, the same sequence works well:

  1. Look up the reference data once. GET /api/v1/chart-of-accounts (expense accounts have type Expense, Overhead, or DirectCosts) and GET /api/v1/tax-rates. Cache the ids.
  2. Find or create the supplier. GET /api/v1/contacts?type=supplier&search=City%20Taxi — if there’s no match, POST /api/v1/contacts with "contactType": "supplier".
  3. 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=…. Ignore VOIDED bills; if any other bill comes back, skip the receipt (or just attach the file). This makes a re-run or a retried request safe.
  4. Create the bill — POST /api/v1/bills (below). Leave it as a draft if a person should review it, or — with the bills:approve scope — send "status": "APPROVED" to post it straight away.
  5. Attach the receipt — POST /api/v1/bills/{id}/attachments with 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.
  6. Say where it came from — pass a note when creating the bill. It lands in the bill’s History & Notes, for example “Total read from receipt photo; tax not shown”.

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.

Terminal window
curl "https://app.accountfun.com/api/v1/bills?reference=RCPT-1001" \
-H "Authorization: Bearer $ACCESS_TOKEN"
ParameterTypeDefaultDescription
statusstring—Filter by status: DRAFT, SUBMITTED, APPROVED, PAID, or VOIDED.
contactIdstring—Filter to bills from a specific supplier.
referencestring—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.
pageinteger1Page number (1-based, max 10000).
limitinteger25Page size (max 200).
{
"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 }
}

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.

Terminal window
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"
}
]
}'
FieldTypeRequiredDescription
contactIdstringYesThe supplier. Must be an active contact in the token’s organization.
issueDatedateYesThe bill / receipt date, "YYYY-MM-DD".
dueDatedateYes"YYYY-MM-DD", on or after issueDate. For an already-paid receipt, use the same date.
linesarrayYes1–500 line items (see below).
statusstringNo"DRAFT" (default) or "APPROVED" to post it in the same call. Approving needs the bills:approve scope and the approve permission.
referencestringNoThe supplier’s invoice or receipt number. Max 200 characters. Recommended — it’s what GET /api/v1/bills?reference= matches on.
currencystringNo3-letter ISO 4217 code. Defaults to the organization’s base currency. Must be enabled in the organization.
exchangeRatestringNoRate to the base currency for a foreign-currency bill. Omit to use the day’s rate.
billNumberstringNoYour 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.
taxModestringNoexclusive (amounts exclude tax), inclusive (amounts include tax), or none. Defaults to the organization’s purchases setting.
projectIdstringNoTag the bill to an open project.
notestringNoAdded 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).

FieldTypeRequiredDescription
quantitystringYesNumeric string, >= 0, up to 4 decimal places.
unitAmountstringYesNumeric string, up to 4 decimal places; may be negative (e.g. a discount line).
descriptionstringNoMax 500 characters.
accountIdstringNo*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.
taxRateIdstringNoAn active tax rate from GET /api/v1/tax-rates. Omit, null, or "" for no tax.
itemIdstringNoLink the line to a product or service. A tracked item adds the quantity to stock when the bill is approved.
discountstringNoA 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.

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"
}

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).

StatusCause
400Invalid 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).
400The 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.
400A line uses an account it can’t — the message names the line and the account. See Accounts a line can’t use.
403The contact, an account, or an item does not belong to this organization.
403The issue date falls on or before the organization’s lock date.
403Insufficient 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.
409Duplicate billNumber, or a bill rule was violated (for example a negative total — use a supplier credit note instead).

Terminal window
curl https://app.accountfun.com/api/v1/bills/7c0e5a1f-... \
-H "Authorization: Bearer $ACCESS_TOKEN"
{
"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 (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.

Terminal window
curl -X POST https://app.accountfun.com/api/v1/bills/7c0e5a1f-.../approve \
-H "Authorization: Bearer $ACCESS_TOKEN"
{ "billId": "7c0e5a1f-...", "status": "APPROVED", "journalEntryId": "..." }

status is the bill’s status after posting — a zero-total bill goes straight to PAID with journalEntryId: null.

StatusCause
400The bill can’t be approved in its current state (already approved, paid, or voided).
403insufficient_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.
403The issue date falls on or before the lock date.
404No bill with that id in the token’s organization.
409A posting rule failed (for example no exchange rate for the bill date), or the organization’s Accounts Payable / tax system accounts are missing.

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 .xlsm renamed to .xlsx is 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.

Send multipart/form-data with one or more file parts:

Terminal window
curl -X POST https://app.accountfun.com/api/v1/bills/7c0e5a1f-.../attachments \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-F "file=@receipt-1001.pdf"

If your tool can only send JSON — common for low-code automation — send the file as base64 instead:

Terminal window
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)\"}]}"
FieldTypeDescription
files[].fileNamestringFile name with its extension — the extension decides the file type.
files[].contentBase64stringThe file’s bytes, base64-encoded. A data:…;base64, prefix is accepted.
{
"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.

StatusCause
400No file, invalid base64, every file was rejected, or the bill already has 10 attachments.
403Insufficient role (view-only roles can’t upload).
404No bill with that id in the token’s organization.
413The request is over 25 MB, or the organization’s storage quota is full.
503Virus scanning is temporarily unavailable — retry later. Nothing unscanned is stored.

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.

Terminal window
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
}
}
  • url opens 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.

  • uploadUrl takes the files directly, as multipart/form-data, with no Authorization header: the link is the authorization.

    Terminal window
    curl -F "file=@receipt-1001.jpg" https://app.accountfun.com/upload/9f2c…/files

    It answers 201 with { attachments, warnings?, filesRemaining }, 404 for an unknown link, 410 once the link has expired, has taken all its files, or its app was disconnected, and 403 if 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.

Terminal window
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:

Terminal window
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.

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.

StatusMeaning
DRAFTSaved, not posted. Editable in AccountFUN.
SUBMITTEDAwaiting approval.
APPROVEDPosted — the expense and what you owe are in the books.
PAIDFully paid (balanceDue is zero).
VOIDEDCancelled 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.