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

# Journal Entries and Period Close

> Post a balanced manual journal entry, handle approvals, and close an accounting period via the API.

## What this guide covers

This guide walks through two closely related tasks:

1. **Manual journal entries** -- posting adjusting, accrual, or correcting entries with balanced lines
2. **Period close** -- locking a period after all entries are in so no accidental postings slip through

Most GL entries are posted automatically by Arcus when business events occur (order fulfillment, payments, receipts). Manual JEs are for adjusting entries, accruals, depreciation, and corrections that do not originate from a transaction.

## Prerequisites

* API key with scope `accounting:write` (create JEs)
* API key with scope `accounting:approve` (approve JEs above threshold)
* API key with scope `accounting:close_period` (close periods)
* Valid `account_id` values for postable GL accounts (use `GET /v1/gl-accounts?postable_only=true`)
* An open accounting period that covers your `entry_date`

***

## The two non-negotiable rules

Before writing any JE code, understand the two rules Arcus enforces at every write path:

### 1. Debits must equal credits

Every journal entry must balance: the sum of all `debit` amounts must equal the sum of all `credit` amounts. An off-by-one-cent imbalance is a validation failure.

```
Rule: SUM(debit) == SUM(credit)
```

If the amounts do not balance, the API returns 422 with `code: gl_imbalance` and includes the exact difference:

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

### 2. Post to leaf accounts only

You cannot post to header (rollup) accounts like "1000 Cash and Bank Accounts" or "1400 Inventory." You must post to a leaf account like "1011 Operating Checking" or "1410 Finished Goods Inventory."

Use `GET /v1/gl-accounts?postable_only=true` to get the list of valid accounts for your entity.

```json theme={null}
{
  "error": "account_is_header",
  "code": "account_is_header",
  "type": "validation_error",
  "hint": "GL account 1000 is a header account and cannot be posted to directly.",
  "param": "lines[0].account_id"
}
```

***

## Posting a manual journal entry

### Example: prepaid insurance amortization

A $1,200 annual insurance policy paid upfront. Each month, $100 moves from Prepaid Expenses to Insurance Expense.

<CodeGroup>
  ```bash curl theme={null}
  curl -s -X POST "https://api.arcuserp.com/v1/journal-entries" \
    -H "Authorization: Bearer $ARCUS_API_KEY" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: je-insurance-amort-2026-05" \
    -d '{
      "entry_date": "2026-05-31",
      "description": "Prepaid insurance amortization - May 2026",
      "memo": "Acme General Liability Policy #GL-2026-001",
      "lines": [
        {
          "account_id": "gl_insurance_expense_uuid",
          "debit": 100.00,
          "description": "Insurance expense May"
        },
        {
          "account_id": "gl_prepaid_expenses_uuid",
          "credit": 100.00,
          "description": "Release prepaid"
        }
      ]
    }'
  ```

  ```typescript TypeScript SDK theme={null}
  import Arcus from "@arcuserp/sdk";

  const arcus = new Arcus({ apiKey: process.env.ARCUS_API_KEY });

  const je = await arcus.journalEntries.create({
    entry_date: "2026-05-31",
    description: "Prepaid insurance amortization - May 2026",
    memo: "Acme General Liability Policy #GL-2026-001",
    lines: [
      {
        account_id: "gl_insurance_expense_uuid",
        debit: 100.00,
        description: "Insurance expense May",
      },
      {
        account_id: "gl_prepaid_expenses_uuid",
        credit: 100.00,
        description: "Release prepaid",
      },
    ],
  }, { headers: { "Idempotency-Key": "je-insurance-amort-2026-05" } });

  console.log(je.data.id);     // je_uuid
  console.log(je.data.status); // posted (or pending_approval if above threshold)
  console.log(je.data.je_number); // JE-00128
  ```

  ```python Python SDK theme={null}
  import arcuserp, os

  client = arcuserp.Arcus(api_key=os.environ["ARCUS_API_KEY"])

  je = client.journal_entries.create(
      entry_date="2026-05-31",
      description="Prepaid insurance amortization - May 2026",
      memo="Acme General Liability Policy #GL-2026-001",
      lines=[
          {
              "account_id": "gl_insurance_expense_uuid",
              "debit": 100.00,
              "description": "Insurance expense May",
          },
          {
              "account_id": "gl_prepaid_expenses_uuid",
              "credit": 100.00,
              "description": "Release prepaid",
          },
      ],
      extra_headers={"Idempotency-Key": "je-insurance-amort-2026-05"},
  )

  print(je.data.id)
  print(je.data.status)  # posted or pending_approval
  ```
</CodeGroup>

### Response shape

```json theme={null}
{
  "data": {
    "id": "je_uuid_abc",
    "je_number": "JE-00128",
    "entry_date": "2026-05-31",
    "description": "Prepaid insurance amortization - May 2026",
    "status": "posted",
    "total_debits": 100.00,
    "total_credits": 100.00,
    "source_type": "MANUAL",
    "lines": [
      {
        "id": "jel_uuid_1",
        "account_id": "gl_insurance_expense_uuid",
        "account_number": "6100",
        "account_name": "Insurance Expense",
        "debit": 100.00,
        "credit": 0.00,
        "description": "Insurance expense May"
      },
      {
        "id": "jel_uuid_2",
        "account_id": "gl_prepaid_expenses_uuid",
        "account_number": "1310",
        "account_name": "Prepaid Expenses",
        "debit": 0.00,
        "credit": 100.00,
        "description": "Release prepaid"
      }
    ]
  }
}
```

***

## Approval workflow

If your entity has approval thresholds configured, journal entries above the low threshold land in `pending_approval` status. GL does not post until the entry is approved.

Check the status:

```bash theme={null}
curl "https://api.arcuserp.com/v1/journal-entries/$JE_ID" \
  -H "Authorization: Bearer $ARCUS_API_KEY"
# Check: data.status
```

Approve (requires `accounting:approve` scope; approver must be a different user than the creator):

```bash theme={null}
curl -s -X POST "https://api.arcuserp.com/v1/journal-entries/$JE_ID/approve" \
  -H "Authorization: Bearer $ARCUS_API_KEY"
```

The status transitions to `posted` and the GL lines are active.

***

## Reversing a posted entry

GAAP requires an audit trail: you cannot modify or delete a posted journal entry. To undo one, create a reversing entry:

```bash theme={null}
curl -s -X POST "https://api.arcuserp.com/v1/journal-entries/$JE_ID/reverse" \
  -H "Authorization: Bearer $ARCUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "reversal_date": "2026-06-01",
    "description": "Reverse JE-00128 - incorrect account"
  }'
```

The reversal is a new JE with all debits and credits swapped. The original entry is unchanged and both remain visible in the account ledger. The response includes `is_reversing: true` and `reversal_of_id` pointing to the original.

***

## Idempotency

Use a stable `Idempotency-Key` tied to your source record. For recurring entries, use a key that includes the period:

```
je-monthly-rent-2026-05
je-depreciation-equipment-2026-05
je-insurance-amort-2026-05
```

Replaying the same key within 24 hours returns the original response without creating a duplicate entry.

***

## Closing an accounting period

When all entries for a period are in, close the period to prevent accidental future postings.

### Step 1: Run the pre-close checklist

```bash theme={null}
curl "https://api.arcuserp.com/v1/periods/$PERIOD_ID/close-checklist" \
  -H "Authorization: Bearer $ARCUS_API_KEY"
```

The checklist reports blocking issues that should be resolved before close:

```json theme={null}
{
  "data": {
    "period_id": "per_uuid",
    "period_name": "May 2026",
    "blocking_issues": [],
    "warnings": [
      {
        "type": "open_vendor_bills",
        "count": 2,
        "message": "2 vendor bills are open and unpaid."
      }
    ],
    "clearing_account_drains": [
      {
        "account": "stripe_clearing",
        "balance": 0.00,
        "status": "ok"
      }
    ],
    "is_closeable": true
  }
}
```

`is_closeable: true` means no blocking issues. Warnings are advisory.

Common blocking issues:

| Issue                                  | What to do                                                      |
| -------------------------------------- | --------------------------------------------------------------- |
| Unposted journal entries in the period | Post or delete the draft entries                                |
| Pending-approval journal entries       | Approve or reject them                                          |
| Clearing account `blocking_issue`      | Investigate unexplained drift in a marketplace clearing account |

### Step 2: Request close (two-person flow)

For separation of duties, use the two-step close:

```bash theme={null}
# Step 2a: Request (any accounting:close_period user)
curl -s -X POST "https://api.arcuserp.com/v1/periods/$PERIOD_ID/request-close" \
  -H "Authorization: Bearer $ARCUS_API_KEY"

# Step 2b: Approve (a different user with accounting:close_period + accounting:approve)
curl -s -X POST "https://api.arcuserp.com/v1/periods/$PERIOD_ID/approve-close" \
  -H "Authorization: Bearer $SECOND_USER_API_KEY"
```

### Step 2 (alternative): Force close

Owners and admins can close immediately without a second approver:

```bash theme={null}
curl -s -X POST "https://api.arcuserp.com/v1/periods/$PERIOD_ID/close" \
  -H "Authorization: Bearer $ARCUS_API_KEY"
```

The period transitions to `closed` status. Any attempt to create a journal entry with an `entry_date` inside a closed period returns:

```json theme={null}
{
  "error": "period_closed",
  "code": "period_closed",
  "type": "validation_error",
  "hint": "Accounting period May 2026 is closed. Use a date in an open period or reopen this period."
}
```

***

## Reopening a period

If you need to post a correcting entry after close:

```bash theme={null}
curl -s -X POST "https://api.arcuserp.com/v1/periods/$PERIOD_ID/reopen" \
  -H "Authorization: Bearer $ARCUS_API_KEY"
```

The period returns to `open` status. Reopen + correcting entry + re-close is the standard GAAP path for post-close adjustments. All changes are audit-logged.

***

## Year-end closing entries

At fiscal year-end, revenue and expense account balances are swept into Retained Earnings (account 3000). As of 2026-05-13, Arcus exposes `POST /v1/accounting_periods/{id}/close-fiscal-year` for automated closing-entry generation per GAAP:

```bash theme={null}
# Automated year-end close (recommended): posts the canonical closing JE
# sweeping revenue/expense -> Retained Earnings in one idempotent call.
curl -s -X POST "https://api.arcuserp.com/v1/fiscal-periods/$PERIOD_ID/close-fiscal-year" \
  -H "Authorization: Bearer $ARCUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "fiscal_year": 2026,
    "fy_start_date": "2026-01-01",
    "fy_end_date": "2026-12-31"
  }'
# Returns: { data: { je_id, fiscal_year, net_income_loss, balanced: true, idempotent } }
# Re-running returns the existing JE without double-posting (idempotent).
```

**Precondition:** all monthly periods in the fiscal year must be in `closed` or `pending_approval` status before calling this endpoint.

**Balance sheet after close:** `GET /v1/reports/balance-sheet` returns `retained_earnings` from the booked account 3000 balance plus `current_year_earnings` showing any un-closed post-close activity separately.

**Manual closing entries (power-user alternative):** if you need per-account line control, you can still post manual JEs through `POST /v1/journal-entries`:

```bash theme={null}
# Manual close -- revenue into retained earnings (power user)
curl -s -X POST "https://api.arcuserp.com/v1/journal-entries" \
  -H "Authorization: Bearer $ARCUS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: year-end-close-revenue-FY2026" \
  -d '{
    "entry_date": "2026-12-31",
    "description": "FY2026 year-end close - revenue to retained earnings",
    "source_type": "fiscal_year_close",
    "lines": [
      { "account_id": "gl_sales_revenue_uuid",   "debit": 485000.00 },
      { "account_id": "gl_retained_earnings_uuid", "credit": 485000.00 }
    ]
  }'
```

Repeat for COGS and expense accounts (the net becomes your net income for the year). After posting all closing entries, the income statement accounts should show zero balances and only balance sheet accounts should have non-zero balances.

***

## Running reports to verify

Before closing, confirm the trial balance is in balance:

```bash theme={null}
curl "https://api.arcuserp.com/v1/reports/trial-balance?period_id=$PERIOD_ID" \
  -H "Authorization: Bearer $ARCUS_API_KEY"
# Check: data.is_balanced == true, data.total_debits == data.total_credits
```

Check AR and AP subledger reconciliation:

```bash theme={null}
curl "https://api.arcuserp.com/v1/reports/subledger-reconciliation?period_id=$PERIOD_ID" \
  -H "Authorization: Bearer $ARCUS_API_KEY"
# Check: ar_control_balance == ar_subledger_balance
#        ap_control_balance == ap_subledger_balance
```

***

## Common errors

| Code                      | HTTP | Meaning                                                                   |
| ------------------------- | ---- | ------------------------------------------------------------------------- |
| `gl_imbalance`            | 422  | Debit sum does not equal credit sum                                       |
| `account_is_header`       | 422  | Line targets a rollup account; use a leaf account                         |
| `account_inactive`        | 422  | Account is deactivated; reactivate or use a different account             |
| `account_not_found`       | 422  | account\_id does not exist for this entity                                |
| `period_closed`           | 422  | entry\_date falls in a closed period                                      |
| `self_approval_forbidden` | 422  | The approver is the same user as the creator (SoD violation)              |
| `period_not_closeable`    | 422  | Checklist has blocking issues; resolve them first                         |
| `insufficient_scope`      | 403  | Missing accounting:write, accounting:approve, or accounting:close\_period |

***

## Next steps

* [GL Fundamentals](/guides/general-ledger-fundamentals) -- chart of accounts structure, system account keys, and financial reports
* [Purchase-to-Pay Flow](/guides/purchase-to-pay-flow) -- how PO receipts and vendor bills post GL entries automatically
* [API Reference: Journal Entries](/api-reference/accounting/list-journal-entries)
* [API Reference: Accounting Periods](/api-reference/accounting/list-accounting-periods)
* [API Reference: Financial Reports](/api-reference/accounting/trial-balance)
