# Bills

> Create, fetch, and approve supplier bills and attach receipts through the AccountFUN public API — including a recommended workflow for importing expenses in bulk or from an AI assistant.

Source: https://www.accountfun.com/docs/api/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](https://www.accountfun.com/docs/api/authentication/).

## 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](https://www.accountfun.com/docs/api/contacts/) (the supplier), the [chart of accounts](https://www.accountfun.com/docs/api/chart-of-accounts/) (the expense account) and [tax rates](https://www.accountfun.com/docs/api/tax-rates/). AI assistants can use all of this through the [MCP server](https://www.accountfun.com/docs/api/mcp/).

***

## 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:

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](#upload-links) 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”.

Receipts usually show tax-inclusive totals

If you only know the amount paid, send `"taxMode": "inclusive"` and use the total as the line’s `unitAmount` with the right `taxRateId`; AccountFUN splits out the tax for you. Use `"taxMode": "none"` when there’s no tax at all.

Already paid?

Creating and approving a bill records what you owe. If the expense was already paid (by card or bank transfer), match the payment to the bill with **Find & Match** when you [reconcile the bank account](https://www.accountfun.com/docs/guides/bank-reconciliation/) — the bill then shows as **Paid**.

***

## 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

```bash
curl "https://app.accountfun.com/api/v1/bills?reference=RCPT-1001" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

### 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`

```json
{
  "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

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

```bash
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

| 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](https://www.accountfun.com/docs/guides/projects/).                                                                                        |
| `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

| 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](#accounts-a-line-cant-use). |
| `taxRateId`   | string | No       | An active tax rate from [`GET /api/v1/tax-rates`](https://www.accountfun.com/docs/api/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

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:

```json
{
  "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`

The created bill in the same shape as [Fetch a single bill](#fetch-a-single-bill), including its number, computed `subtotal` / `taxTotal` / `total`, and lines.

```json
{
  "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

| 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](#accounts-a-line-cant-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

```bash
curl https://app.accountfun.com/api/v1/bills/7c0e5a1f-... \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

### Response — `200 OK`

```json
{
  "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

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.

```bash
curl -X POST https://app.accountfun.com/api/v1/bills/7c0e5a1f-.../approve \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

### Response — `200 OK`

```json
{ "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

| 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

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](https://www.accountfun.com/docs/guides/files/#upload-security-and-processing): documents are antivirus-checked and can remain pending until cleared; images are resized and re-encoded as JPEG with metadata removed.

### Multipart upload

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

```bash
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)

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

```bash
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`

```json
{
  "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

| 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

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.

```bash
curl -X POST https://app.accountfun.com/api/v1/bills/7c0e5a1f-.../upload-links \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

No body. Response — `201 Created`:

```json
{
  "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.

  ```bash
  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](#attach-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

```bash
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.

***

## Notes

Read and add notes in the bill’s **History & Notes**:

```bash
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

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

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