# Invoices

> List, create, fetch, and approve invoices through the AccountFUN public API.

Source: https://www.accountfun.com/docs/api/invoices/

The invoices endpoints let your integration read sales documents, push new invoices into an organization’s books, and approve them so they’re posted to the ledger — without anyone having to click anything inside AccountFUN.

Reading requires the `invoices:read` scope; creating draft invoices requires `invoices:write`. **Approving** is a higher-trust action (it posts to the ledger), so it needs its own scope, `invoices:approve`, and the authorizing user’s role must have the `approve` permission (**Approve Documents** — the built-in Admin, Accountant and Standard roles have it; Sales and Purchase don’t). See [Authentication](https://www.accountfun.com/docs/api/authentication/).

Dates

Invoice dates — `issueDate`, `dueDate` and `settlementRateDate` — are plain calendar dates written `YYYY-MM-DD` (for example `"2026-08-11"`), with no time and no timezone. They mean the same day whatever the organization’s timezone is, and changing that timezone never moves them. Timestamps such as `createdAt`, `updatedAt`, `postedAt` and `voidedAt` are full ISO 8601 instants in UTC (`"2026-08-11T14:23:00.000Z"`).

## Endpoints

| Method | Path                                 | Scope              | Description                                                                   |
| ------ | ------------------------------------ | ------------------ | ----------------------------------------------------------------------------- |
| `GET`  | `/api/v1/invoices`                   | `invoices:read`    | List invoices (paginated).                                                    |
| `POST` | `/api/v1/invoices`                   | `invoices:write`   | Create a draft invoice — or an approved one, with `invoices:approve` as well. |
| `POST` | `/api/v1/invoices/{id}/approve`      | `invoices:approve` | Approve (post) a draft or submitted invoice.                                  |
| `GET`  | `/api/v1/invoices/{id}`              | `invoices:read`    | Fetch a single invoice with line items.                                       |
| `GET`  | `/api/v1/invoices/{id}/attachments`  | `invoices:read`    | List the invoice’s attached files.                                            |
| `POST` | `/api/v1/invoices/{id}/attachments`  | `invoices:write`   | Attach files to the invoice (multipart or JSON base64).                       |
| `POST` | `/api/v1/invoices/{id}/upload-links` | `invoices:write`   | Mint a 15-minute, upload-only link for someone who holds the file.            |
| `GET`  | `/api/v1/invoices/{id}/notes`        | `invoices:read`    | List the invoice’s History & Notes.                                           |
| `POST` | `/api/v1/invoices/{id}/notes`        | `invoices:write`   | Add a note to the invoice’s History & Notes.                                  |

***

## List invoices

Returns a paginated list of invoices for the token’s organization, newest first. The list response includes summary fields and the contact — **not** the line items — so it stays small. Fetch a single invoice to get its lines.

### Request

```bash
curl https://app.accountfun.com/api/v1/invoices \
  -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 invoices for a specific contact (a contact id).                |
| `page`      | integer | `1`     | Page number (1-based, max `10000`).                                      |
| `limit`     | integer | `25`    | Page size (max `200`).                                                   |

```plaintext
GET /api/v1/invoices?status=APPROVED&page=2&limit=50
```

### Response — `200 OK`

```json
{
  "invoices": [
    {
      "id": "1a2b3c4d-...",
      "invoiceNumber": "INV-0101",
      "status": "APPROVED",
      "currency": "USD",
      "total": "490.00",
      "amountPaid": "0.00",
      "balanceDue": "490.00",
      "issueDate": "2026-08-11",
      "dueDate": "2026-09-10",
      "createdAt": "2026-08-11T14:23:00.000Z",
      "updatedAt": "2026-08-11T14:23:00.000Z",
      "contact": {
        "id": "00000000-0000-0000-0000-000000000001",
        "name": "Northwind Traders",
        "email": "ap@northwind.example"
      }
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 25,
    "total": 48,
    "totalPages": 2
  }
}
```

| Field                   | Description                                    |
| ----------------------- | ---------------------------------------------- |
| `invoices[].total`      | Invoice total as a string (decimal).           |
| `invoices[].amountPaid` | Amount already allocated against this invoice. |
| `invoices[].balanceDue` | Outstanding amount (`total − amountPaid`).     |
| `invoices[].status`     | Current lifecycle status.                      |
| `pagination.total`      | Total matching invoices across all pages.      |
| `pagination.totalPages` | Number of pages at the current `limit`.        |

***

## Create an invoice

Creates a new invoice. By default the invoice is created in **`DRAFT`** status — saved, but not yet posted to the ledger. Add `"status": "APPROVED"` to the request body to create **and** approve in a single call (see [Approve on creation](#approve-on-creation)).

This requires `invoices:write`. The authorizing user’s role must also have the `create` permission (**Create Drafts**) and include sales documents — the built-in Admin, Accountant, Standard and Sales roles do.

### Request

```bash
curl -X POST https://app.accountfun.com/api/v1/invoices \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "contactId": "00000000-0000-0000-0000-000000000001",
    "issueDate": "2026-08-11",
    "dueDate": "2026-09-10",
    "currency": "USD",
    "lines": [
      {
        "description": "Widget order #1042",
        "quantity": "2",
        "unitAmount": "49.00",
        "accountId": "00000000-0000-0000-0000-00000000000a",
        "taxRateId": "00000000-0000-0000-0000-0000000000aa"
      }
    ]
  }'
```

### Body parameters

| Field                  | Type                | Required | Description                                                                                                                                                                                                                |
| ---------------------- | ------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `contactId`            | string              | Yes      | The contact (customer) to bill. Must be active and belong to the token’s organization.                                                                                                                                     |
| `issueDate`            | date (`YYYY-MM-DD`) | Yes      | The invoice date.                                                                                                                                                                                                          |
| `dueDate`              | date (`YYYY-MM-DD`) | Yes      | Must be on or after `issueDate`.                                                                                                                                                                                           |
| `lines`                | array               | Yes      | One or more line items (see below).                                                                                                                                                                                        |
| `status`               | string              | No       | `"DRAFT"` (default) or `"APPROVED"`. Set to `"APPROVED"` to create and post the invoice in one step — see [Approve on creation](#approve-on-creation). Requires the `invoices:approve` scope and the `approve` permission. |
| `currency`             | string              | No       | 3-letter ISO 4217 code. Defaults to the organization’s base currency. Must be an enabled currency in the org.                                                                                                              |
| `exchangeRate`         | string              | No       | Exchange rate to the base currency (for foreign-currency invoices).                                                                                                                                                        |
| `invoiceNumber`        | string              | No       | A reference number. If omitted, one is generated. Max 50 characters.                                                                                                                                                       |
| `reference`            | string              | No       | Internal reference. Max 200 characters.                                                                                                                                                                                    |
| `taxMode`              | string              | No       | `exclusive`, `inclusive`, or `none`. Defaults to the organization’s setting.                                                                                                                                               |
| `fallbackCosts`        | object              | No       | Cost overrides for tracked items sold before any stock establishes a cost — a map of `itemId` → unit cost (string). See [Tracked items](#when-the-cost-is-unknown-cold-start).                                             |
| `remindersDisabled`    | boolean             | No       | If `true`, automatic payment reminders are suppressed for this invoice.                                                                                                                                                    |
| `allowOnlinePayment`   | boolean             | No       | If `true`, enables online payment on the invoice.                                                                                                                                                                          |
| `projectId`            | string              | No       | Put the invoice in an open [project](https://www.accountfun.com/docs/guides/projects/) for the same customer.                                                                                                              |
| `settlementCurrency`   | string              | No       | Show the customer the amount due in this (enabled) currency. Omit to use the customer’s default; `null` settles in the invoice currency.                                                                                   |
| `settlementRateSource` | string              | No       | `system` (default — the day’s rate) or `manual`.                                                                                                                                                                           |
| `settlementRate`       | string              | No       | The rate to use when `settlementRateSource` is `manual`.                                                                                                                                                                   |

### Line item parameters

| Field         | Type   | Required | Description                                                                                                                                                                                                                                                                                                                                                                                                       |
| ------------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `quantity`    | string | Yes      | Numeric string, `>= 0` (e.g. `"2"`, `"1.5"`).                                                                                                                                                                                                                                                                                                                                                                     |
| `unitAmount`  | string | Yes      | Numeric string; may be negative (e.g. `"49.00"`, `"-10.00"`).                                                                                                                                                                                                                                                                                                                                                     |
| `description` | string | No       | Line description. Max 500 characters. Defaults to `""`.                                                                                                                                                                                                                                                                                                                                                           |
| `accountId`   | string | No\*     | The income account to post to. **Required for non-zero lines.** Not one of the [accounts a line can’t use](#accounts-a-line-cant-use).                                                                                                                                                                                                                                                                            |
| `taxRateId`   | string | No       | A 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](https://www.accountfun.com/docs/guides/items/). For a **tracked** item this drives automatic stock movements and cost-of-goods-sold when the invoice is approved — see [Items and tracked inventory](#items-and-tracked-inventory-on-invoice-lines). The API does **not** auto-fill the line from the item; send `description`, `unitAmount`, and `accountId` explicitly. |
| `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`; any non-zero line must have one.

Sending dates

Send dates as `YYYY-MM-DD`. A full ISO 8601 date-time is also accepted for compatibility: only the date as written is used, and the time and offset are ignored (`"2026-08-11T00:00:00+08:00"` is August 11). An impossible date such as `"2026-02-30"` returns `400`.

Money values are strings

All monetary and quantity fields are **strings**, not numbers. This avoids floating-point rounding. Send values like `"49.00"`, not `49`. An invoice’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 money received with Record Payment or Receive 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`.

Revenue, expense, cost-of-sales, 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 1: \"Business Account\" is a bank account — record money received with Record Payment or Receive Money, not as an invoice line"
}
```

### Approve on creation

Add `"status": "APPROVED"` to the request body to create the invoice **and** post it to the ledger in a single atomic call. The invoice goes straight to `APPROVED`: revenue, tax, and receivables are recorded immediately, and any [tracked-item](#items-and-tracked-inventory-on-invoice-lines) lines reduce stock on hand and record their cost of goods sold.

```bash
curl -X POST https://app.accountfun.com/api/v1/invoices \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "contactId": "00000000-0000-0000-0000-000000000001",
    "issueDate": "2026-08-12",
    "dueDate": "2026-09-11",
    "status": "APPROVED",
    "lines": [
      {
        "itemId": "00000000-0000-0000-0000-0000000000bb",
        "description": "Widget (tracked)",
        "quantity": "2",
        "unitAmount": "49.00",
        "accountId": "00000000-0000-0000-0000-00000000000a"
      }
    ]
  }'
```

Because approving posts to the books, it needs the `invoices:approve` scope as well as `invoices:write`, and the authorizing user’s role must have the `approve` permission. Without either, sending `"status": "APPROVED"` returns `403` with a message naming `invoices:approve` — nothing is created and nothing reaches the ledger.

Draft vs approve-on-creation

Use `"status": "APPROVED"` when your integration owns the sale end-to-end — for example, a store pushing a completed order straight into the books. Leave the default `DRAFT` when a person should review the invoice inside AccountFUN before it’s posted, then [approve it later](#approve-an-invoice).

### Response — `201 Created`

```json
{
  "invoice": {
    "id": "1a2b3c4d-...",
    "invoiceNumber": "INV-0102",
    "status": "DRAFT",
    "issueDate": "2026-08-11",
    "dueDate": "2026-09-10",
    "subtotal": "98.00",
    "taxTotal": "0.00",
    "total": "98.00",
    "journalEntryId": null,
    "...": "..."
  }
}
```

The full invoice is returned — the same shape as [Fetch a single invoice](#fetch-a-single-invoice), including its generated number, dates, computed totals, and lines. Two fields reflect the lifecycle step:

| Field                    | Description                                                                                            |
| ------------------------ | ------------------------------------------------------------------------------------------------------ |
| `invoice.status`         | `"DRAFT"` by default, or `"APPROVED"` if you approved on creation (`"PAID"` for a zero-total invoice). |
| `invoice.journalEntryId` | `null` for a draft. For an approved invoice, the id of the posted ledger entry.                        |

### Validation errors

| Status | Cause                                                                                                                                                                                                                                                       |
| ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | Invalid body (not JSON, missing required fields, a date that isn’t a valid `YYYY-MM-DD` day, unknown currency, an id that isn’t a valid id, an invalid `discount`, or a total over 999,999,999,999,999.9999). The `details` field lists each invalid field. |
| `400`  | The contact is deactivated, an account is archived, a tax rate is inactive or not in the organization, or the project is closed or belongs to another customer.                                                                                             |
| `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 or an account does not belong to this organization.                                                                                                                                                                                             |
| `403`  | The issue date falls on or before the organization’s lock date.                                                                                                                                                                                             |
| `403`  | Insufficient role — creating needs the `create` permission.                                                                                                                                                                                                 |
| `403`  | `"status": "APPROVED"` without the `invoices:approve` scope or the `approve` permission. The message names `invoices:approve`; nothing is created — send it as a draft instead.                                                                             |
| `409`  | An invoice-number conflict or other invoice-rule violation.                                                                                                                                                                                                 |
| `409`  | `needsCostConfirmation` — approving a tracked item with no established cost. Resend with `fallbackCosts` (see [Tracked items](#when-the-cost-is-unknown-cold-start)).                                                                                       |

***

## Items and tracked inventory on invoice lines

Each invoice line can carry an optional `itemId` linking it to a [product or service](https://www.accountfun.com/docs/guides/items/). What that link does depends entirely on whether the item is set up to **track inventory**.

The API never auto-fills a line from its item

Unlike AccountFUN’s in-app line editor — which copies an item’s description, price, account, and tax onto the line for you — the API uses exactly the values you send. Always include `description`, `quantity`, `unitAmount`, `accountId`, and `taxRateId` explicitly. `itemId` is a link, not a template.

### When the item is untracked (or has no itemId)

If `itemId` is omitted, points to an item that **isn’t** inventory-tracked, or doesn’t exist in the organization’s Products & Services, it behaves as a plain reference tag. The line is costed and posted like any other invoice line using the values you supplied — there are **no stock movements** and **no cost-of-goods entries**. The `itemId` is simply stored on the line for your record-keeping and for reporting inside AccountFUN.

### When the item is tracked

A tracked item is one with **Track inventory** switched on in Products & Services — typically physical goods you buy and resell. When an invoice line references a tracked item **and the invoice is approved**, AccountFUN does the inventory bookkeeping automatically:

| Effect             | What happens                                                                             |
| ------------------ | ---------------------------------------------------------------------------------------- |
| **Stock reduced**  | Quantity on hand drops by the line `quantity`.                                           |
| **Cost recorded**  | The units sold are costed at the current **weighted-average cost** (WAC).                |
| **Journal posted** | Dr *Cost of Goods Sold* / Cr *Inventory Asset*, folded into the invoice’s posting entry. |

This happens **only at approval**, never when a draft is created. So whether you approve on creation (`"status": "APPROVED"`) or call the [approve endpoint](#approve-an-invoice) later, the stock and cost entries land at that moment.

Tip

You don’t do anything special to trigger this — just put the tracked item’s `itemId` on the line and approve the invoice. AccountFUN works out the cost and posts the journals itself.

### Zero-value lines still consume stock

A line can carry a quantity but a unit amount of `"0"` — a free sample, a giveaway, a promotional unit. On a **tracked** item that line still uses up real stock: approving it relieves the units at their average cost and posts the Dr COGS / Cr Inventory entry, even though there’s no revenue on the line. Giving inventory away still consumes the asset, so AccountFUN records it.

(A zero-total line is allowed to omit `accountId`; a tracked zero-value line posts its COGS/Inventory legs regardless.)

### Selling stock you don’t have (backorders)

You can sell a tracked item even when there isn’t enough on hand — the quantity on hand simply goes negative, and the sale is still costed at the current average. When more stock is bought in later, the average cost recalculates as usual. The one requirement is that the item needs a **known cost** to relieve against (see below).

### When the cost is unknown (cold start)

To relieve stock at a cost, AccountFUN needs to know what each unit cost. It gets that from the **average cost**, which is established the first time you buy the item on a bill. If you approve a sale of a tracked item that has **never been purchased** — so no average cost exists yet — and the item has no cost price on its record, AccountFUN won’t guess.

Both approval paths return a `409` that names exactly which items need a cost:

```json
{
  "error": "Cost confirmation required for tracked item(s) sold before any stock was received",
  "needsCostConfirmation": true,
  "items": ["00000000-0000-0000-0000-0000000000bb"]
}
```

Resolve it by sending `fallbackCosts` — a map of item id → unit cost, as a string in the organization’s **base currency** — and retrying:

* **On create:** add `"fallbackCosts": { "item-id": "12.50" }` alongside `"status": "APPROVED"`.
* **On the approve endpoint:** send `{ "fallbackCosts": { "item-id": "12.50" } }` as the request body.

```bash
curl -X POST https://app.accountfun.com/api/v1/invoices/1a2b3c4d-.../approve \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "fallbackCosts": { "00000000-0000-0000-0000-0000000000bb": "12.50" } }'
```

Treat `409 needsCostConfirmation` as a recoverable state in your integration: look up the item’s usual cost (its purchase price, or a value your user configures), send it back, and the approval completes.

***

## Approve an invoice

Approve (post) a `DRAFT` or `SUBMITTED` invoice so it’s recorded in the ledger. This is the same action as clicking **Approve** inside AccountFUN.

```plaintext
POST /api/v1/invoices/{id}/approve
```

Requires the `invoices:approve` scope and the `approve` permission. Approving posts to the ledger, which is a higher-trust action than creating a draft.

### Request

```bash
curl -X POST https://app.accountfun.com/api/v1/invoices/1a2b3c4d-.../approve \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

### Body parameters

The body is optional. Send it only when resolving a cold-start tracked item (see [When the cost is unknown](#when-the-cost-is-unknown-cold-start)).

| Field           | Type   | Description                                                                                     |
| --------------- | ------ | ----------------------------------------------------------------------------------------------- |
| `fallbackCosts` | object | Map of `itemId` → unit cost (string, base currency) for tracked items with no established cost. |

### Response — `200 OK`

```json
{
  "invoiceId": "1a2b3c4d-...",
  "status": "APPROVED",
  "journalEntryId": "je-abc123"
}
```

`status` is the invoice’s status after posting — a zero-total invoice goes straight to `PAID`, with `journalEntryId: null` when nothing was posted.

### Errors

| Status | Cause                                                                                                                                                                                      |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `400`  | The invoice can’t be approved in its current state (e.g. already approved, paid, or voided).                                                                                               |
| `403`  | `insufficient_scope` — the token doesn’t carry `invoices:approve` (`invoices:write` and `bills:approve` don’t approve invoices), or the user’s role doesn’t have the `approve` permission. |
| `403`  | The issue date falls on or before the organization’s lock date.                                                                                                                            |
| `404`  | No invoice with that id exists in the token’s organization.                                                                                                                                |
| `409`  | `needsCostConfirmation` — a tracked item has no cost basis. Resend with `fallbackCosts`.                                                                                                   |
| `409`  | The organization’s Accounts Receivable / tax system accounts are missing.                                                                                                                  |

***

## Fetch a single invoice

Returns one invoice with its full detail, including line items (with their account and tax rate) and the contact.

### Request

```bash
curl https://app.accountfun.com/api/v1/invoices/1a2b3c4d-... \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

### Response — `200 OK`

```json
{
  "invoice": {
    "id": "1a2b3c4d-...",
    "invoiceNumber": "INV-0101",
    "status": "APPROVED",
    "currency": "USD",
    "total": "490.00",
    "amountPaid": "0.00",
    "balanceDue": "490.00",
    "issueDate": "2026-08-11",
    "dueDate": "2026-09-10",
    "contact": { "id": "...", "name": "Northwind Traders", "..." : "..." },
    "lines": [
      {
        "id": "...",
        "description": "Widget order #1042",
        "quantity": "2",
        "unitAmount": "245.00",
        "accountId": "00000000-...",
        "account": { "id": "...", "code": "400", "name": "Sales Revenue", "..." : "..." },
        "taxRateId": "00000000-...",
        "taxRate": {
          "id": "...",
          "name": "Tax on Sales (10%)",
          "kind": "SIMPLE",
          "rate": "10",
          "isActive": true,
          "isSystem": false,
          "components": []
        }
      }
    ]
  }
}
```

For a component-based rate (`"kind": "COMPONENT"`, e.g. GST + QST), each entry in `taxRate.components` is `{ "id", "componentId", "sortOrder", "component": { "id", "name", "rate", "recoverable" } }`, in order. These are the same component fields as [`GET /api/v1/tax-rates`](https://www.accountfun.com/docs/api/tax-rates/).

### Response — `404 Not Found`

Returned if no invoice with that `id` exists in the token’s organization. The org scoping is server-side, so an `id` from a different organization simply returns `404` — it never leaks the invoice’s existence.

***

## Attach files

Attach one or more files to an invoice — at any status. Use this for source documents (a purchase-order scan, a signed copy); it does not change the invoice.

* **File types:** PDF, PNG, JPEG, XLSX, XLS, DOCX, DOC, CSV. For PDF, image and Office files the content must match the extension.
* **Limits:** 5 MB per file, 25 MB per request, 10 attachments per invoice, 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

```bash
curl -X POST https://app.accountfun.com/api/v1/invoices/7c0e5a1f-.../attachments \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -F "file=@po-1001.pdf"
```

### JSON upload (base64)

```bash
curl -X POST https://app.accountfun.com/api/v1/invoices/7c0e5a1f-.../attachments \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"files\": [{\"fileName\": \"po-1001.pdf\", \"contentBase64\": \"$(base64 -w0 po-1001.pdf)\"}]}"
```

| 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": "po-1001.pdf", "mimeType": "application/pdf", "fileSize": 48213, "createdAt": "2026-10-07T03:13:10.004Z" }
  ]
}
```

When several files are sent and some are rejected, the stored ones are returned with a `warnings` array explaining the others. `400` if no file was stored or the invoice already has 10; `403` for a view-only role; `404` if the invoice isn’t in the token’s organization; `413` over 25 MB or when the storage quota is full; `503` while virus scanning is unavailable.

### List attachments

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

Returns `{ "attachments": [ … ] }` with each file’s `id`, `originalName`, `mimeType`, `fileSize`, `scanStatus` and `createdAt`, oldest first.

### Upload links

When the caller can’t send the file itself, it can mint a short-lived link and hand it to whoever holds the file (an AI assistant in a chat uses this):

```bash
curl -X POST https://app.accountfun.com/api/v1/invoices/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 no-sign-in page where a person picks the file; `uploadUrl` takes the files directly as `multipart/form-data` with no `Authorization` header (`curl -F "file=@po-1001.pdf" https://app.accountfun.com/upload/9f2c…/files`). A link works for **15 minutes**, takes up to **5 files**, can only **add** files to that one invoice, and stops working when the app is disconnected or loses `invoices:write`, or when its creator can no longer change the invoice. Uploads follow the same rules as [attaching files](#attach-files).

***

## Notes

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

```bash
curl -X POST https://app.accountfun.com/api/v1/invoices/7c0e5a1f-.../notes \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"body": "PO number from the customer."}'
```

`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 one typed in AccountFUN. Notes are up to 2000 characters.

***

## Invoice statuses

| Status      | Meaning                                                             |
| ----------- | ------------------------------------------------------------------- |
| `DRAFT`     | Created but not yet posted to the ledger. Editable.                 |
| `SUBMITTED` | Sent for approval; awaiting an approver.                            |
| `APPROVED`  | Posted to the ledger — revenue recorded, customer owes the balance. |
| `PAID`      | Fully paid (`balanceDue` is zero).                                  |
| `VOIDED`    | Cancelled; reversed out of the ledger.                              |

Omitting `status` (or sending `"DRAFT"`) on create produces a `DRAFT`. Sending `"status": "APPROVED"` on create, or calling `POST /api/v1/invoices/{id}/approve`, moves an invoice to `APPROVED` over the API — both need `invoices:approve`. Other lifecycle steps (submit, void) are done inside AccountFUN.
