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.
Endpoints
Section titled “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
Section titled “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
Section titled “Request”curl https://app.accountfun.com/api/v1/invoices \ -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 invoices for a specific contact (a contact id). |
page | integer | 1 | Page number (1-based, max 10000). |
limit | integer | 25 | Page size (max 200). |
GET /api/v1/invoices?status=APPROVED&page=2&limit=50Response — 200 OK
Section titled “Response — 200 OK”{ "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
Section titled “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).
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
Section titled “Request”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
Section titled “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. 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. |
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 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
Section titled “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. |
taxRateId | string | No | A 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. For a tracked item this drives automatic stock movements and cost-of-goods-sold when the invoice is approved — see Items and tracked inventory. 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.
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 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:
{ "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
Section titled “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 lines reduce
stock on hand and record their cost of goods sold.
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.
Response — 201 Created
Section titled “Response — 201 Created”{ "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, 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
Section titled “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. |
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). |
Items and tracked inventory on invoice lines
Section titled “Items and tracked inventory on invoice lines”Each invoice line can carry an optional itemId linking it to a product or
service. What that link does depends entirely on whether the
item is set up to track inventory.
When the item is untracked (or has no itemId)
Section titled “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
Section titled “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 later, the stock and cost entries land
at that moment.
Zero-value lines still consume stock
Section titled “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)
Section titled “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)
Section titled “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:
{ "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.
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
Section titled “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.
POST /api/v1/invoices/{id}/approveRequires the invoices:approve scope and the approve permission. Approving
posts to the ledger, which is a higher-trust action than creating a draft.
Request
Section titled “Request”curl -X POST https://app.accountfun.com/api/v1/invoices/1a2b3c4d-.../approve \ -H "Authorization: Bearer $ACCESS_TOKEN"Body parameters
Section titled “Body parameters”The body is optional. Send it only when resolving a cold-start tracked item (see When the cost is unknown).
| Field | Type | Description |
|---|---|---|
fallbackCosts | object | Map of itemId → unit cost (string, base currency) for tracked items with no established cost. |
Response — 200 OK
Section titled “Response — 200 OK”{ "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
Section titled “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
Section titled “Fetch a single invoice”Returns one invoice with its full detail, including line items (with their account and tax rate) and the contact.
Request
Section titled “Request”curl https://app.accountfun.com/api/v1/invoices/1a2b3c4d-... \ -H "Authorization: Bearer $ACCESS_TOKEN"Response — 200 OK
Section titled “Response — 200 OK”{ "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.
Response — 404 Not Found
Section titled “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
Section titled “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: 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”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)
Section titled “JSON upload (base64)”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
Section titled “Response — 201 Created”{ "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
Section titled “List attachments”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
Section titled “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):
curl -X POST https://app.accountfun.com/api/v1/invoices/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 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.
Read and add notes in the invoice’s History & Notes:
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
Section titled “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.