Overview
Arcus uses a full double-entry general ledger (GL). Every dollar movement in the system — fulfilled orders, received payments, refunds, purchase receipts, vendor bills, inventory adjustments — posts a balanced journal entry. No financial transaction occurs without a corresponding GL record. The GL is the backbone: if the trial balance is in balance (debits = credits), all downstream reports (balance sheet, income statement, AR aging, AP aging) are correct by construction.Chart of Accounts
Each entity has its own chart of accounts (COA) stored inaccounting.gl_accounts.
Accounts are organized into a hierarchy:
Header vs Leaf Accounts
Accounts fall into two categories:-
Header accounts (
is_header=true) — rollup parents (e.g.1000 Cash and Bank Accounts,1400 Inventory). They aggregate their children for reporting. You cannot post journal entry lines to a header account. Attempting to do so returns HTTP 422 with codeaccount_is_header. -
Leaf accounts (
is_header=false, is_active=true) — the only valid targets for journal entry lines (e.g.1300 Accounts Receivable,4000 Sales Revenue,5000 Cost of Goods Sold).
?postable_only=true:
System Account Keys
Arcus’s posting helpers (e.g.postFulfillmentEntries, postPaymentReceived) resolve
accounts by a stable string key rather than account number:
GET /v1/gl-accounts?system_account_key=ar_account to resolve any key to a UUID.
System accounts cannot be created or modified via the API.
Journal Entries
Anatomy of a Journal Entry
Every journal entry (accounting.journal_entries) has:
- entry_date — must fall within an open accounting period
- description — human-readable label
- source_type — identifies the business event (MANUAL, FULFILLMENT, PAYMENT, etc.)
- lines — two or more lines; exactly one of
debitorcreditis > 0 per line
The Two Invariants (P0)
1. Balance (DR = CR): The sum of all debit amounts must equal the sum of all credit amounts. Arcus enforces this inroutes/gl.mjs::assertGLBalance() before any INSERT.
A mismatch returns HTTP 422:
account_id must reference a leaf,
active account owned by the entity. Arcus enforces this in assertGLLinesPostable() and
also at the database layer via a BEFORE INSERT trigger (migration 165).
Error codes:
account_is_header— you referenced a rollup accountaccount_inactive— account is deactivatedaccount_not_found— UUID does not exist for this entityaccount_wrong_entity— account belongs to a different entity; entity isolation enforcedline_has_both_debit_and_credit— a single line has both values > 0
Approval Workflow
Your entity may have approval thresholds configured:
Owners and admins bypass the threshold and always auto-approve.
Use
POST /v1/journal-entries/:id/approve (scope accounting:approve) to approve.
Separation of duties is enforced: the approver must differ from the creator.
Reversals
To undo a posted journal entry, usePOST /v1/journal-entries/:id/reverse. This creates
an inverted copy (all debits become credits and vice versa). The original entry is
immutable — GAAP requires an audit trail, so Arcus never modifies or deletes posted
entries.
The reversal response is the new offsetting entry with is_reversing=true and
reversal_of_id pointing to the original.
Idempotency
Pass anIdempotency-Key header on POST /v1/journal-entries to make creation safe
against retries:
Accounting Periods
The GL is segmented into accounting periods (accounting.accounting_periods). Each period
has a start_date, end_date, and status of open, close_requested, or closed.
Closed periods reject new postings. Attempting to create a JE with an entry_date
inside a closed period returns HTTP 422 with code period_closed.
Close Workflow
Before closing, check the pre-close checklist:- Unposted or draft journal entries
- Pending-approval journal entries
- Unreconciled bank accounts
- Open vendor bills
POST /v1/periods/:id/request-close— initiates the request (scope:accounting:close_period)POST /v1/periods/:id/approve-close— a different user approves (scopes:accounting:close_period+accounting:approve)
Financial Reports
All reports are synchronous (no job queue). Pass aperiod_id or date range:
Trial Balance and the Balance Guarantee
The trial balance endpoint sums leaf accounts only (is_header=false). When the GL
is in balance, is_balanced=true and total_debits == total_credits. Any imbalance
indicates a data integrity issue (likely a header-account posting that bypassed the
assertGLLinesPostable guard).
Bank Accounts
Bank accounts (accounting.bank_accounts) link a real bank account to a GL account.
When Plaid sync is active, transactions are automatically pulled and can be matched
during bank reconciliation.
plaid_item_id, plaid_access_token, etc.) are read-only and managed
by the Plaid connector in Settings > Integrations.
Recurring Journal Entries
Use recurring templates for entries that post on a fixed schedule (monthly depreciation, insurance amortization, rent, etc.):next_run_date and advances by the frequency.
Idempotency key per run: recurring:<template_id>:<next_run_date>.
To post immediately, use POST /v1/recurring-journal-entries/:id/run.
Required API Key Scopes
Common Error Codes
See Also
- Authentication guide — how to obtain and scope API keys
- Sales Tax Liability Report — per-jurisdiction filing-ready report
- API Reference: /v1/gl-accounts
- API Reference: /v1/journal-entries
- API Reference: /v1/periods
- API Reference: /v1/reports

