> ## Documentation Index
> Fetch the complete documentation index at: https://docs.arcuserp.com/llms.txt
> Use this file to discover all available pages before exploring further.

# General Ledger Fundamentals

> How Arcus's double-entry GL works, what every API caller needs to know about journal entries, accounts, and period close.

## 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:

| 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 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`:

```http theme={null}
GET /v1/gl-accounts?postable_only=true
Authorization: Bearer ark_live_ent_acme_...
```

### System Account Keys

Arcus's posting helpers (e.g. `postFulfillmentEntries`, `postPaymentReceived`) resolve
accounts by a stable string key rather than account number:

```
ar_account       -> Accounts Receivable (1300)
cogs             -> Cost of Goods Sold (5000)
revenue          -> Sales Revenue (4000)
inventory_fg     -> Finished Goods Inventory (1410)
sales_tax_payable -> Sales Tax Payable (2100)
```

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

```json theme={null}
{
  "entry_date": "2025-06-15",
  "description": "Prepaid insurance June installment",
  "source_type": "MANUAL",
  "lines": [
    { "account_id": "<prepaid_expenses_uuid>", "debit": 500.00 },
    { "account_id": "<cash_operating_bank_uuid>", "credit": 500.00 }
  ]
}
```

### 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:

```json theme={null}
{
  "error": "gl_imbalance",
  "code": "gl_imbalance",
  "hint": "GL posting imbalance [MANUAL]: debits=500.00 credits=0.00 diff=500.0000"
}
```

**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:

| 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                               |

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:

```http theme={null}
POST /v1/journal-entries
Idempotency-Key: je-monthly-rent-2025-06
```

***

## 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:

```http theme={null}
GET /v1/periods/:id/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):**

```http theme={null}
POST /v1/periods/:id/close
```

***

## Financial Reports

All reports are synchronous (no job queue). Pass a `period_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.

```http theme={null}
POST /v1/bank-accounts
Content-Type: application/json

{
  "name": "First National Checking",
  "gl_account_id": "<cash_operating_bank_uuid>",
  "account_type": "checking"
}
```

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.):

```http theme={null}
POST /v1/recurring-journal-entries
Content-Type: application/json

{
  "name": "Monthly rent accrual",
  "frequency": "monthly",
  "next_run_date": "2025-07-01",
  "template_lines": [
    { "account_id": "<rent_expense_uuid>", "debit": 3000.00 },
    { "account_id": "<prepaid_expenses_uuid>", "credit": 3000.00 }
  ]
}
```

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

| 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](/guides/managing-api-keys) -- how to obtain and scope API keys
* [Sales Tax Liability Report](/support/accounting/sales-tax-liability) -- per-jurisdiction filing-ready report
* [API Reference: /v1/gl-accounts](/api-reference/accounting/list-gl-accounts-chart-of-accounts)
* [API Reference: /v1/journal-entries](/api-reference/accounting/list-journal-entries)
* [API Reference: /v1/periods](/api-reference/accounting/list-accounting-periods)
* [API Reference: /v1/reports](/api-reference/accounting/trial-balance)
