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). 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 automatic postings (fulfillment, payments received, and so on) 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 has:- entry_date — must fall within an open accounting period and inside the entity’s posting window (see below)
- 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 checks this before it writes the entry. A mismatch returns HTTP 422:account_id must reference a leaf, active
account owned by the entity. Arcus checks this on every posting path, and checks it again
when the line is stored, so no path can write a line to a header or inactive account.
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
The Posting Window
Besides an open period,POST /v1/journal-entries checks the entry date against the entity’s
posting window, which keeps a mistyped year from landing a manual entry months away from where
it belongs:
- Backdating: an entry dated further back than the entity’s backdate window (180 days by
default) is refused with 422
backdate_beyond_allowed_range. - Future dating: an entry dated further ahead of today (UTC) than the entity’s forward window
(0 days by default, so tomorrow is already outside it) is refused with 422
future_beyond_allowed_range. The body names the window (window_days) and says whether an override is available (override_available) and which permission it takes (required_permission).
backdate_reason for an older date or
future_reason for a later one (4 to 500 characters). The override is granted only when the
identity behind the call holds the accounting.close_period permission and an accounting period
covers the date (a monthly period for a future date); the usual open-period check still applies.
The grant and the reason are recorded on the entry’s audit row. A reason sent by an identity
without that permission is refused 403, and the other refusals are 422 backdate_override_reason_invalid,
future_override_reason_invalid, backdate_override_requires_accounting_period and
future_override_requires_accounting_period.
bypass_period_check is for migration only: it needs the migration:write scope, is silently
ignored for every other key, and every use is audit-logged.
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; it posts the
entry on its own entry_date. Separation of duties is enforced: the approver must differ from
the creator. The forward window is read again at approval, so an entry that was dated within it
when created but now sits further ahead of today than the window is refused 422
future_beyond_allowed_range unless the approver sends future_reason on the same terms as
above (the approver holds accounting.close_period, the reason is 4 to 500 characters, and an
open monthly accounting period covers the date). A granted override is recorded on the approval’s
own audit row. The approval can also be refused 422 future_override_reason_invalid,
future_override_requires_accounting_period or future_override_period_check_failed.
Reversals
To undo a posted journal entry, usePOST /v1/journal-entries/:id/reverse (scope
accounting:write). 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 body is optional and takes one field,
reason. It is written into the reversing entry’s description (Reversal of JE-00128: <reason>); without it the description readsReversal of JE-00128: Reversed via API. - The reversing entry is dated the day of the call (UTC), not the original’s date.
- The response is the new offsetting entry as it was posted:
source_typeisMANUAL_REVERSALandsource_idnames the original. Arcus then links the two, storingis_reversing=trueandreversal_of_idon the new entry andreversed_by_idon the original; read those links back withGET /v1/journal-entries/{id}. - Only a
postedentry can be reversed (409je_not_posted), an entry can be reversed once (409je_already_reversed), and a reversing entry cannot itself be reversed (409je_target_is_reversing; its original is already reversed, so post a correcting entry instead).
unreconciled, and the entry’s leg on that bank account is no longer marked
as cleared. The response lists every freed line in released_bank_transactions (an empty
array when there were none), each with bank_transaction_id, bank_account_id,
link_kind, and the line’s own transaction_date and amount. A payout line whose match
had posted a settlement correction also carries settlement_corrections_still_posted:
that correction stays posted, and only the bank line’s own undo reverses it. A line that
sits in a completed reconciliation (status reconciled) is not released; re-open or
cancel that reconciliation first.
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. Each period has astart_date, end_date,
and a status:
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-closeinitiates the request (scope:accounting:close_period). The period moves fromopentopending_approval. -
POST /v1/periods/:id/approve-closeis sent by a different user (scopes:accounting:close_period+accounting:approve). The body takes optionalnotesandacknowledge_old_period. Before it closes the period, the approval:- refuses when the approver is the user who requested the close (403
sod_self_approval_forbidden) - refuses when the period is not
pending_approval(409period_not_in_pending_approval) - re-runs the checklist and refuses while it reports blocking issues (409
close_blocked_by_checklist) - checks that the period’s posted debits equal its credits (409
period_checksum_failedwhen they do not)
closed. - refuses when the approver is the user who requested the close (403
accounting:close_period closes the period in one call. The checklist still
applies: while it reports blocking issues the call returns 409 close_blocked_by_checklist,
with the blocking items in warnings.
Old periods need an acknowledgement. Closing or approving the close of a period that
ended more than 180 days ago requires "acknowledge_old_period": true in the body.
Without it, both calls return 422 period_too_old_for_simple_close, carrying
period_age_days and max_age_days_without_ack (180) so your client can show the
period’s age. Closing an old period is routine on books that are behind; the flag only
confirms you meant it. On a period within 180 days, omit it and nothing changes.
The key acts as its creator. For the period calls above and for reversals, Arcus checks
the key’s scope and also checks that the user who created the API key holds the matching
permission in their role; if that user does not, the call returns 403 permission_denied.
Financial Reports
All reports are synchronous (no job queue). Most take aperiod_id or date range; AR
aging is the exception (see below):
AR aging, AP aging and the subledger reconciliation all age against the entity’s current business
day and reject the deprecated
as_of_date parameter with 400 unsupported_param, so omit it on
those three reports.
AR Aging
GET /v1/reports/ar-aging (scope accounting:read) ages open receivables against the
entity’s current business day. It returns every customer with an open balance in the
standard buckets; the account_id and buckets parameters listed in the API reference are
not applied by this release, so filter the rows on your side. It has no as-of rewind:
as_of_date is rejected with 400 unsupported_param, so omit it.
Each row and the totals block carry current_amount, days_1_30, days_31_60,
days_61_90, days_90_plus, total and gross_total. For exposure net of open customer
credit, each row adds credit_balance and net_total, and the totals block sums them as
total_credit_balance and net_total.
Billed not shipped. When the entity’s invoice timing (Settings > Payments & Checks > AR &
Invoicing, Invoice Timing) is Per shipment or Invoice what has shipped
(consolidated), every row and the totals block also carry billed_not_shipped: value
invoiced for goods that have not left the warehouse yet. It sits outside current_amount,
outside every day bucket and outside the overdue total, because nothing is owed until
delivery, but it is inside total, so the six buckets still sum to total exactly and an
existing tie to your outstanding AR figure is unaffected. With the other invoice timings
the field is absent and the payload is unchanged.
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 posting to a header account that bypassed the
postable-account check).
Bank Accounts
Bank accounts link a real bank account to a GL account. When a bank feed is connected, transactions arrive automatically and can be matched during bank reconciliation.name and gl_account_id are required; gl_account_id must be a GL account owned by
your entity. Optional fields are institution, is_ap_account (true when the account pays
vendor bills) and account_type, which is checking, savings, money_market, cd,
clearing or other on a detail GL account marked as a bank account, or credit_card,
line_of_credit or loan on a credit-normal liability GL account (a card is
credit_card, not credit, and a mismatch returns 422 gl_normal_balance_mismatch); a
deposit_layout sent on create is ignored and the default slip layout is stored. The call
needs scope accounting:write.
Bank feed fields (plaid_item_id, plaid_access_token, and so on) are stripped from the
request body. The feed itself is connected in the app, on Finance > Close & Reconcile >
Bank Reconciliation, Bank Feed tab (Connect a bank).
The create call returns 201 with the stored bank account; read the resolved sync_status
and has_feed from GET /v1/bank-accounts/{id}. has_feed is true only when the account is
mapped to a connected bank feed AND that bank connection still works: a mapped account whose
connection was removed stops producing lines, so it reads false.
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

