General ledger
The general ledger is the heart of Accounts. Every document flow — AR, AP, payments, expenses and banking — reaches the ledger through one posting seam, so there is exactly one place that turns business events into balanced journals.
Core objects
Section titled “Core objects”- Chart of accounts — GL accounts, each tied to an account type that carries
its category (
ASSET,LIABILITY,EQUITY,REVENUE,EXPENSE) and its normal balance (DEBIT/CREDIT). Control accounts are found by type code (AR, AP, BANK, CASH, EQUITY, EXPENSE, REVENUE, customer advance, inventory, COGS) rather than by name. - Journal entries and journal lines — every line moves exactly one side (debit or credit); a journal’s lines must sum equal on both sides.
- Accounting events — the audit seam a document raises before its journal is written, linking the journal back to the source document.
- Fiscal periods — with per-module status so a period can be closed for one area at a time.
The posting seam
Section titled “The posting seam”A document endpoint, inside its own transaction, raises an accounting event, writes
a DRAFT journal and its lines, then posts it. Everything runs on the caller’s
transaction: if anything fails — including a fiscal-period lock — the document
change, the event and the journal roll back together as one unit.
The seam enforces the double-entry invariant before it writes:
- Zero-amount lines are dropped; at least one real line must remain.
- Each line must move exactly one side (debit or credit, never both).
- Total debits must equal total credits to two decimal places, or the post is rejected as an unbalanced journal.
Journals and reversals
Section titled “Journals and reversals”- Journals move
DRAFT → POSTED, and a posted journal is protected from edits. - Correcting a posted journal is done by reversal: a new posted counter-entry
with debits and credits swapped, dated as needed, linked back to the original,
which is then marked
REVERSED. Both statuses continue to count toward balances, so the reversal nets the original out to zero. - Manual journals can be created, posted and reversed directly; automatic journals come from the document flows.
Fiscal periods
Section titled “Fiscal periods”- Periods carry a status and a per-module breakdown (GL, AR, AP, banking, expense, fixed assets, or all).
- Posting into a closed or locked period is rejected with a clear “period closed” error rather than silently allowed.
Accounting dimensions
Section titled “Accounting dimensions”Journal lines can carry cost center, department, branch and project dimensions. Source documents thread their dimensions onto the lines they post, and reversals carry the same dimensions so the ledger nets out per dimension. The trial balance and P&L can then be filtered to a single dimension slice.
Multi-currency and base amounts
Section titled “Multi-currency and base amounts”Journal lines hold amounts in the document currency, and every journal records the exchange rate it was booked at. So that the financial statements never add a USD invoice to an INR bill at par, each line also carries a base-currency amount: the face amount times the journal’s booked rate. For a single-currency company the rate is 1, base equals face, and nothing about the ledger changes.
The document posting seam and manual journals share one helper for this, and it absorbs the rounding residual on the last debit and last credit line, so a journal balances in base currency as well as in document currency. Reversals are booked at the original journal’s rate, so base movement reverses along with face movement.
What reports in base
Section titled “What reports in base”The base amounts are internal to the ledger: they drive the summarised statements — trial balance, profit & loss, balance sheet, cash flow, budget vs actual and the dashboard, all of which read the balance view that sums them — rather than appearing on the journal screen. Everything that shows you individual entries stays in the currency the document was raised in: the journal list and detail, the general-ledger account view and the day book all read as posted, with the journal’s own currency shown.
Reports sets the currency basis out report by report.
Where the rate comes from
Section titled “Where the rate comes from”Three paths resolve a rate for you — invoice creation, bill creation and draft edit, and recurring-invoice generation. On those, Accounts works down this order:
- A rate entered on the document always wins — a desk that booked a forward contract knows better than any feed.
- A document in the base currency (or with no currency set) is honestly 1.0.
- Otherwise the newest stored rate for that pair effective on or before the document’s date is used.
- If there is none on file, the save is rejected with
422 NO_EXCHANGE_RATE— never a silent 1.0. The message names the currency pair and the date and tells you to refresh the rate feed or send an explicit rate; in the invoice and bill editor it arrives as a “Failed to save” toast carrying that text, and the document is not created.
Every other path books the rate the request carries, and falls back to 1.0 when it carries none. That covers manual journals, customer and vendor payments, expenses, credit notes, retainers and editing a draft invoice — so on those, set the rate explicitly whenever the document is not in your base currency.
Rates live under Settings → Exchange rates: stored per company to six decimal places, effective-dated with an optional expiry, refreshable in one click from the ECB feed (which reports the currencies it does not carry instead of inventing them), overridable by hand with manual rates badged as such, and testable with a rate-check card that runs the same resolver a document save would.
Errors and access control
Section titled “Errors and access control”These two behaviours hold across the whole Accounts API, not only the ledger.
- Database constraints answer, they do not crash. A malformed UUID or date, a
raced duplicate, a reference to a record that does not exist, or a value a CHECK
forbids comes back as
400 INVALID_VALUE,400 INVALID_DATE,409 DUPLICATE,422 INVALID_REFERENCEor422 CONSTRAINT_VIOLATIONrather than a bare 500. The messages are deliberately generic and never name a constraint or column; anything not caused by the caller still returns 500. - Permissions gate every data route. One gate derives the permission it needs
from the route group and the HTTP method —
journal-entries:read,gl-accounts:write— so a route added later is gated by default, and a denial returns403 PERMISSION_DENIED. Enforcement is on by default in production; a deployment can be switched to warn-only, logging what it would have denied and letting the request through, which is how development and test environments are usually run and how a production box still mid-migration can be unblocked. See Roles & permissions.
Connects to
Section titled “Connects to”- Receivables & payables and expenses & purchase orders — the document flows that post here.
- Reports — the trial balance, P&L and balance sheet are read straight off posted (and reversed) journal movement.
- Audit trail — posts and reversals are recorded.