The Ledger Rule & Document Lifecycle

How to tell what is on the ledger: the posted flag, each document type's states, and what the API does with drafts, templates, quotes and voids.

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 posted is true.

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:

  • state is not. It differs per type and includes values that look final but are not on the ledger.
  • number is 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):

  1. Fetch with include_drafts=true so voids and draft changes reach you.
  2. Store posted alongside each row.
  3. Treat a row that flips from posted: true to posted: false as a void: remove its effect from your ledger but keep the document.
  4. 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.

Browse Topics