Skip to main content

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 in accounting.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 code account_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).
When building a journal entry line picker, always filter with ?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:
Use 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 debit or credit is > 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 in routes/gl.mjs::assertGLBalance() before any INSERT. A mismatch returns HTTP 422:
2. Postable accounts only (Rule 21): Every line’s 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 account
  • account_inactive — account is deactivated
  • account_not_found — UUID does not exist for this entity
  • account_wrong_entity — account belongs to a different entity; entity isolation enforced
  • line_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, use POST /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 an Idempotency-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:
The checklist reports blocking issues:
  • Unposted or draft journal entries
  • Pending-approval journal entries
  • Unreconciled bank accounts
  • Open vendor bills
Two-person separation of duties (recommended):
  1. POST /v1/periods/:id/request-close — initiates the request (scope: accounting:close_period)
  2. POST /v1/periods/:id/approve-close — a different user approves (scopes: accounting:close_period + accounting:approve)
Force-close (owner authority):

Financial Reports

All reports are synchronous (no job queue). Pass a period_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 fields (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.):
The scheduler posts the entry on 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