The rule
Every accounting document in Beeswax (invoice, expense, quote, payment, manual journal, bank transfer, payroll, refund) is a journal entry with a state. Only some states put an entry on the ledger, and the API gives you exactly one flag to tell them apart:
An entry is on the ledger only when
postedistrue.
posted appears on every journal-entry-shaped response: the untyped /journal_entries listing, every typed listing (/invoices, /expenses, /payments, /quotes, /raw_journal_entries, and the long-tail types through /journal_entries?type=), and every /…/{id} show. It is derived from the one definition the ledger, trial balance, tax reports and Xero sync themselves use, so it cannot drift between document types.
Nothing else is a reliable signal:
stateis not. It differs per type and includes values that look final but are not on the ledger.numberis not. Numbers are issued when a document is created, drafts included. A draft invoice or a draft manual journal can carry a number.- Line items are not. A draft payroll template, a draft invoice or a quote can have fully formed debit and credit lines beneath it. They have no ledger effect until the entry is posted.
If you sum anything (revenue, wages, an account balance), filter on posted: true first.
States per document type
| Type | States that are posted | States that are not posted | Notes |
|---|---|---|---|
| Invoice | finalised, pdf_sent, marked_sent, first_notice, second_notice, final_notice, partial_paid, paid |
draft, voided |
Finalising issues the ledger postings (receivables, tax). Voiding removes them while keeping the document, its number and history. |
| Expense (bill) | finalised, pdf_sent, marked_sent, partial_paid, paid |
draft, voided |
Same as invoices; the notice states are not used. |
| Customer refund, supplier refund | finalised and later |
draft, voided |
Linked to a parent invoice or bill. |
| Payment (income, expense, payroll) | finalised, paid |
— | Created posted by the payment flow. A payment with posted: false is an anomaly, not activity. |
| Manual journal | finalised |
draft |
Created posted by default through the API; status: "draft" stages it. A draft can carry a number. |
| Bank transfer | finalised |
draft |
Created posted. |
| Payroll | finalised, paid |
draft |
Every employee has a payroll template: a draft payroll with wage, tax and superannuation lines already on it. It is never a wage until a real payroll is created from it and finalised. |
| BAS statement journal | finalised |
draft |
The period's summary entry. |
| Quote | — | all states | Quotes never post. posted is always false, whatever the state. |
The account lock date and voiding are described in the Invoices, Expenses and Ledger help; neither changes the rule above.
The write lifecycle
For the documents you can create through the API:
| Step | Invoice / Expense | Quote | Manual journal |
|---|---|---|---|
POST /… |
Lands as a draft, posted: false, number already issued |
Lands as a draft (auto-finalises; still never posts) | Posted immediately, unless status: "draft" |
| Edit | PATCH the header; sections and lines through /transaction_groups |
Same | Drafts only; posted journals are immutable |
POST …/finalise |
draft → finalised, postings written |
Internal state only, no ledger effect | draft → finalised, postings written |
| Undo | No unfinalise. A finalised unpaid document is edited in place; once any payment is applied it is locked (editable: false) |
Locked once marked accepted in the web app | — |
| Remove | DELETE while draft; POST …/void once finalised and unpaid |
DELETE |
DELETE while draft |
editable on invoices, expenses and quotes tells you in advance whether a write will be accepted. Nothing in this lifecycle emails a client: sending is a human action in the web app.
What the API does with unposted entries
Listings default to posted entries. /journal_entries, every typed listing and /raw_journal_entries return only posted: true entries unless you ask otherwise:
| Query | Returns | meta.filter.posted |
|---|---|---|
| (nothing) | posted entries | true |
?posted=false |
unposted entries only (drafts, templates, voided) | false |
?include_drafts=true or ?posted=all |
everything | null |
?state=draft (any explicit state) |
that state; the posted default is switched off | null |
The response meta always says which filter was applied, so a consumer can see what it got. /quotes and ?type=quotes apply no default, since a posted-only quote listing would always be empty.
Transactions endpoints never return postings for an unposted entry. GET /…/{id}/transactions on a draft, template or voided entry returns:
{ "transactions": [], "posted": false, "reason": "draft" }
reason is draft, voided, template or quote. To inspect what a draft would post, pass ?preview=true; every returned line then carries "preview": true and must not be summed into balances. A posted entry returns { "transactions": [...], "posted": true }.
Quotes are the one exception: a quote's lines are its authored content and the quote write API round-trips them here, so they are returned with posted: false, reason: "quote" and each line marked "ledger": false.
Syncing with the rule in mind
When you keep a copy of the books (see Sync & safe retries):
- Fetch with
include_drafts=trueso voids and draft changes reach you. - Store
postedalongside each row. - Treat a row that flips from
posted: truetoposted: falseas a void: remove its effect from your ledger but keep the document. - Treat a draft that disappears as deleted; drafts have no ledger effect, so nothing needs reversing.
The failure this prevents
A consumer reconciling a ledger fetched /journal_entries with no filter, received a draft payroll template among 5,766 entries, fetched its transactions, and summed five wage, tax and superannuation lines into the year's payroll. Beeswax itself never counted them. Under the current contract the template is absent from the default listing, its transactions endpoint returns no lines, and every entry it could have been confused with carries posted: true.
For MCP users
The Beeswax MCP server mirrors this contract from version 1.4.0: listing tools default to posted entries and take posted / include_drafts, transaction tools return { posted, reason, transactions }, and the server's instructions state the rule. See Connect to Claude & AI assistants.