What this guide covers
This guide walks through two closely related tasks:- Manual journal entries — posting adjusting, accrual, or correcting entries with balanced lines
- Period close — locking a period after all entries are in so no accidental postings slip through
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_periodandaccounting: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_deniedon reversals and period calls - Valid
account_idvalues for postable GL accounts (useGET /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 alldebit amounts must equal the sum of all credit amounts. An off-by-one-cent imbalance is a validation failure.
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.” UseGET /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 100 moves from Prepaid Expenses to Insurance Expense.Response shape
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, theentry_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 inpending_approval status. GL does not post until the entry is approved.
Check the status:
accounting:approve scope; approver must be a different user than the creator):
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 (scopeaccounting:write):
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 tounreconciled, 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 stableIdempotency-Key tied to your source record. For recurring entries, use a key that includes the period:
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
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: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 withaccounting:close_period can close the period in one call, without a second approver:
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):"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 anentry_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 (scopeaccounting:close_period):
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 exposesPOST /v1/accounting_periods/{id}/close-fiscal-year for automated closing-entry generation per GAAP:
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:
Running reports to verify
Before closing, confirm the trial balance is in balance:as_of_date parameter with 400 unsupported_param, and subledger_type narrows it to one subledger.
Common errors
Next steps
- GL Fundamentals — chart of accounts structure, system account keys, and financial reports
- Purchase-to-Pay Flow — how PO receipts and vendor bills post GL entries automatically
- API Reference: Journal Entries
- API Reference: Accounting Periods
- API Reference: Financial Reports

