Skip to content
AccountFUNDocs

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.

MethodPathScopeDescription
GET/api/v1/invoicesinvoices:readList invoices (paginated).
POST/api/v1/invoicesinvoices:writeCreate a draft invoice — or an approved one, with invoices:approve as well.
POST/api/v1/invoices/{id}/approveinvoices:approveApprove (post) a draft or submitted invoice.
GET/api/v1/invoices/{id}invoices:readFetch a single invoice with line items.
GET/api/v1/invoices/{id}/attachmentsinvoices:readList the invoice’s attached files.
POST/api/v1/invoices/{id}/attachmentsinvoices:writeAttach files to the invoice (multipart or JSON base64).
POST/api/v1/invoices/{id}/upload-linksinvoices:writeMint a 15-minute, upload-only link for someone who holds the file.
GET/api/v1/invoices/{id}/notesinvoices:readList the invoice’s History & Notes.
POST/api/v1/invoices/{id}/notesinvoices:writeAdd a note to the invoice’s History & Notes.

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.

Terminal window
curl https://app.accountfun.com/api/v1/invoices \
-H "Authorization: Bearer $ACCESS_TOKEN"
ParameterTypeDefaultDescription
statusstring—Filter by status: DRAFT, SUBMITTED, APPROVED, PAID, or VOIDED.
contactIdstring—Filter to invoices for a specific contact (a contact id).
pageinteger1Page number (1-based, max 10000).
limitinteger25Page size (max 200).
GET /api/v1/invoices?status=APPROVED&page=2&limit=50
{
"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
}
}
FieldDescription
invoices[].totalInvoice total as a string (decimal).
invoices[].amountPaidAmount already allocated against this invoice.
invoices[].balanceDueOutstanding amount (total − amountPaid).
invoices[].statusCurrent lifecycle status.
pagination.totalTotal matching invoices across all pages.
pagination.totalPagesNumber of pages at the current limit.

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.

Terminal window
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"
}
]
}'
FieldTypeRequiredDescription
contactIdstringYesThe contact (customer) to bill. Must be active and belong to the token’s organization.
issueDatedate (YYYY-MM-DD)YesThe invoice date.
dueDatedate (YYYY-MM-DD)YesMust be on or after issueDate.
linesarrayYesOne or more line items (see below).
statusstringNo"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.
currencystringNo3-letter ISO 4217 code. Defaults to the organization’s base currency. Must be an enabled currency in the org.
exchangeRatestringNoExchange rate to the base currency (for foreign-currency invoices).
invoiceNumberstringNoA reference number. If omitted, one is generated. Max 50 characters.
referencestringNoInternal reference. Max 200 characters.
taxModestringNoexclusive, inclusive, or none. Defaults to the organization’s setting.
fallbackCostsobjectNoCost overrides for tracked items sold before any stock establishes a cost — a map of itemId → unit cost (string). See Tracked items.
remindersDisabledbooleanNoIf true, automatic payment reminders are suppressed for this invoice.
allowOnlinePaymentbooleanNoIf true, enables online payment on the invoice.
projectIdstringNoPut the invoice in an open project for the same customer.
settlementCurrencystringNoShow the customer the amount due in this (enabled) currency. Omit to use the customer’s default; null settles in the invoice currency.
settlementRateSourcestringNosystem (default — the day’s rate) or manual.
settlementRatestringNoThe rate to use when settlementRateSource is manual.
FieldTypeRequiredDescription
quantitystringYesNumeric string, >= 0 (e.g. "2", "1.5").
unitAmountstringYesNumeric string; may be negative (e.g. "49.00", "-10.00").
descriptionstringNoLine description. Max 500 characters. Defaults to "".
accountIdstringNo*The income account to post to. Required for non-zero lines. Not one of the accounts a line can’t use.
taxRateIdstringNoA tax rate from GET /api/v1/tax-rates. Omit, null, or "" for no tax.
itemIdstringNoLink 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.
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; any non-zero line must have one.

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

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.

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

{
"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:

FieldDescription
invoice.status"DRAFT" by default, or "APPROVED" if you approved on creation ("PAID" for a zero-total invoice).
invoice.journalEntryIdnull for a draft. For an approved invoice, the id of the posted ledger entry.
StatusCause
400Invalid 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.
400The 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.
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 or an account does not belong to this organization.
403The issue date falls on or before the organization’s lock date.
403Insufficient 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.
409An invoice-number conflict or other invoice-rule violation.
409needsCostConfirmation — 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.

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:

EffectWhat happens
Stock reducedQuantity on hand drops by the line quantity.
Cost recordedThe units sold are costed at the current weighted-average cost (WAC).
Journal postedDr 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.

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

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.
Terminal window
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 (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}/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.

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

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

FieldTypeDescription
fallbackCostsobjectMap of itemId → unit cost (string, base currency) for tracked items with no established cost.
{
"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.

StatusCause
400The invoice can’t be approved in its current state (e.g. already approved, paid, or voided).
403insufficient_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.
403The issue date falls on or before the organization’s lock date.
404No invoice with that id exists in the token’s organization.
409needsCostConfirmation — a tracked item has no cost basis. Resend with fallbackCosts.
409The organization’s Accounts Receivable / tax system accounts are missing.

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

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

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 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.
Terminal window
curl -X POST https://app.accountfun.com/api/v1/invoices/7c0e5a1f-.../attachments \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-F "file=@po-1001.pdf"
Terminal window
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)\"}]}"
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": "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.

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

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

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

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


StatusMeaning
DRAFTCreated but not yet posted to the ledger. Editable.
SUBMITTEDSent for approval; awaiting an approver.
APPROVEDPosted to the ledger — revenue recorded, customer owes the balance.
PAIDFully paid (balanceDue is zero).
VOIDEDCancelled; 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.