Skip to main content

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 and reopen periods)
  • API key with scopes accounting:close_period and accounting:approve (approve another user’s close request)
  • The user who created each API key must also hold the matching permission in their role; the key acts as that user, and a missing permission returns 403 permission_denied on reversals and period calls
  • 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.
If the amounts do not balance, the API returns 422 with code: gl_imbalance and includes the exact difference:

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.

Posting a manual journal entry

Example: prepaid insurance amortization

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

Response shape

Lines are sent as debit and credit, and come back as debit_amount and credit_amount. The body also takes optional source_type (defaults to MANUAL), source_id and source_module, and per line an optional description, location_id, department_id, class_id and project_id.

The posting window

Besides an open period, the entry_date must sit inside the entity’s posting window. A date further back than the backdate window (180 days by default) returns 422 backdate_beyond_allowed_range, and a date further ahead of today (UTC) than the forward window (0 days by default) returns 422 future_beyond_allowed_range. To post outside the window on purpose, resend with backdate_reason or future_reason (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, and the reason is recorded on the entry’s audit row. General Ledger Fundamentals lists every refusal code.

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:
Approve (requires accounting:approve scope; approver must be a different user than the creator):
The status transitions to posted and the GL lines are active. The entry posts on its own entry_date, and the forward window is checked again at approval: an entry now dated further ahead of today than the window returns 422 future_beyond_allowed_range unless the approver sends future_reason, on the same terms as above.

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 (scope accounting:write):
The body is optional and takes one field, reason. It becomes part of the reversing entry’s description (Reversal of JE-00128: Incorrect account); without it the description reads Reversal of JE-00128: Reversed via API. There is no date field: the reversing entry is dated the day of the call (UTC). 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 is the new entry as it was posted: source_type is MANUAL_REVERSAL and source_id names the original. Arcus then links the two, storing is_reversing: true and reversal_of_id on the new entry and reversed_by_id on the original. Read those links back with GET /v1/journal-entries/{id} rather than from the reverse response.

Bank lines the reversal frees

If a bank statement line was matched to the original entry, the reversal returns that line to the review queue: the match is cleared, the line goes back to unreconciled, the entry’s leg on that bank account is no longer marked as cleared, and an activity record naming the reversal is written for each line. The response lists the freed lines in released_bank_transactions, an empty array when there were none (response abridged to the relevant fields):
unstamped counts the entry’s lines on that bank account that are no longer marked as cleared (with a reason when none were). link_kind says which match was cleared: journal_entry when the line was matched to the reversed entry itself, marketplace_payout when it was matched to a payout that this entry posted. A marketplace_payout row 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.

When a reversal is refused


Idempotency

Use a stable Idempotency-Key tied to your source record. For recurring entries, use a key that includes the period:
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

The checklist reports blocking issues that should be resolved before close:
can_close: true means blocking_issues is empty. open_vendor_bills and outstanding_ar are each an object with a count and a total, and the unreconciled bank accounts are a count for you to review. The checklist page shows open vendor bills and outstanding receivables as warnings, but the close calls treat them as blockers: POST /periods/{id}/close and approve-close also return 409 close_blocked_by_checklist while open vendor bills or outstanding AR invoices dated in the period remain, even when this checklist shows can_close: true. Each entry in blocking_issues is a sentence naming what to fix, and blocking_issue_details carries the same blockers as numbers. Common blocking issues:

Step 2: Request close (two-person flow)

For separation of duties, use the two-step close:
The request moves the period from open to pending_approval. A period awaiting approval still accepts postings; it stops accepting them once it is closed. The approval moves it to closed, and before it does, it checks the request again: To take back a request that has not been approved yet, POST /v1/periods/$PERIOD_ID/withdraw-close-request (optional body field reason) returns the period to open and clears the request. The person who made the request can withdraw it with accounting:close_period; withdrawing someone else’s request also needs accounting:approve.

Step 2 (alternative): Direct close

A key with accounting:close_period can close the period in one call, without a second approver:
The checklist still applies, including open vendor bills and outstanding AR invoices dated in the period: while it reports blocking issues, the call returns 409 close_blocked_by_checklist with the blocking items in warnings. On success the response is the period with status closed.

Closing a period that ended more than 180 days ago

Both the approval and the direct close ask for an acknowledgement when the period ended more than 180 days ago. Closing an old period seals a range your books may already have been reported from, and reopening it later needs the same confirmation, so Arcus asks first. Without the flag, the call returns 422 (abridged):
Re-send the same call with "acknowledge_old_period": true. This is a confirmation, not a permission: closing an old period is routine on books that are behind. On a period within 180 days, omit the flag and nothing changes.

After the close

Any attempt to create a journal entry with an entry_date inside a closed period returns 422 (abridged):
next_open_period_start is the first day you can post to instead (null when no later period is open).

Reopening a period

If you need to post a correcting entry after close (scope accounting:close_period):
The body fields: The period returns to open. The close tracking fields are cleared, reopened_by and reopened_at are set, and an activity record with the reason and the period’s prior state is written. Reopen + correcting entry + re-close is the standard GAAP path for post-close adjustments. Separation of duties. The user who requested or approved the period’s close cannot reopen it alone; the call returns 403 sod_self_reopen_forbidden. When there is no second person to do it (a single-owner company, for example), send sod_override_authorized_by with the user id of whoever authorizes the reopen. That user must hold an active membership on your entity with the close-period permission, and may be you. The call still records you as the person who reopened the period, and it writes a separate activity record naming the override and who authorized it. When a reopen is refused: A period closed by the fiscal year-end close has status year_end_closed and is not reopened here: use POST /v1/fiscal-periods/{id}/reopen-fiscal-year, which also reverses the closing entry.

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:
The body takes fiscal_year, fy_start_date and fy_end_date, all three required, and the call needs accounting:close_period. POST /v1/accounting_periods/{id}/close-fiscal-year is an alias that accepts the same body. Precondition: all monthly periods in the fiscal year must be in closed or pending_approval status before calling this endpoint.

Reopening a fiscal year

POST /v1/fiscal-periods/{id}/reopen-fiscal-year (scope accounting:close_period) reverses a year-end close. Send fiscal_year and a reason of up to 500 characters. Arcus posts the sign-flipped inverse of the original closing entry, leaves the original entry unchanged, and returns the fiscal year’s last period to closed. Re-running it for a year already reopened returns the existing reversal entry (idempotent: true) instead of posting a second one. 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:
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:
Check AR, AP and inventory subledger reconciliation:
The reconciliation compares balances as of the entity’s current business day. It rejects the deprecated as_of_date parameter with 400 unsupported_param, and subledger_type narrows it to one subledger.

Common errors


Next steps