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:
| Type | Normal Balance | Examples |
|---|---|---|
| asset | debit | AR, Cash, Inventory, Prepaid Expenses |
| liability | credit | AP, Sales Tax Payable, Customer Deposits |
| equity | credit | Retained Earnings, Owner’s Equity |
| revenue | credit | Sales Revenue, Shipping Income |
| expense / cogs | debit | COGS, Freight Out, Merchant Fees |
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:| Amount | Behavior |
|---|---|
<= je_approval_threshold_low | Auto-approved, status = posted |
> low threshold | Requires manager approval, status = pending_approval |
> je_approval_threshold_high | Requires senior approval |
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:
| Endpoint | What it returns |
|---|---|
GET /v1/reports/trial-balance | DR and CR balance per GL account (leaf accounts only) |
GET /v1/reports/profit-loss | Revenue, COGS, gross profit, operating expenses, net income |
GET /v1/reports/balance-sheet | Assets, liabilities, equity as of a date |
GET /v1/reports/cash-flow | Operating, investing, financing cash flows |
GET /v1/reports/account-ledger | Running transaction history for one GL account |
GET /v1/reports/ar-aging | Receivables aging (current, 30, 60, 90, 90+ days) |
GET /v1/reports/ap-aging | Payables aging |
GET /v1/reports/subledger-reconciliation | GL balance vs AR/AP/inventory subledger |
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
| Action | Required Scope |
|---|---|
| Read GL accounts, JEs, reports, periods, bank accounts | accounting:read |
| Create / update GL accounts, JEs, bank accounts, recurring JEs | accounting:write |
| Approve / reject journal entries | accounting:approve |
| Close, reopen, or request-close accounting periods | accounting:close_period |
| Backfill JEs to a closed period (migration only) | migration:write |
Common Error Codes
| Code | HTTP | Meaning |
|---|---|---|
gl_imbalance | 422 | DR sum does not equal CR sum |
account_is_header | 422 | Line targets a rollup account (not postable) |
account_inactive | 422 | Line targets a deactivated account |
account_not_found | 422 | account_id does not exist for this entity |
period_closed | 422 | entry_date falls in a closed accounting period |
self_approval_forbidden | 422 | Approver is the same as the creator (SoD violation) |
system_account_creation_forbidden | 403 | Cannot create system accounts via the API |
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

