Close a period
Closing a period locks it: nothing can be posted into it afterwards. Before it does, the ERP checks a list of conditions (the close checklist) that a controller would otherwise check by hand. This page closes September 2026 of Demo AG: it reads the checklist, runs the month-end runs that the checklist demands, shows an approval that blocks the close, closes the period, shows that the period then refuses postings, reopens it, and reads the integrity evidence of the ledger.
Before you start
- Three kinds of key take part.
$ERP_KEYis the first key of the demo organization (a human key with every scope). The system issued it, so it belongs to no person: it runs the month-end runs and posts, but it can never approve a request.$ALICE_KEY(every scope) is the key of a person, a user who signed in and created a key for themselves; she approves, closes and reopens here. The page also creates an agent key. Getting started explains where such keys come from and conventions who counts as who. - Reading the checklist needs
read, the month-end runs needdraft, and closing or reopening needsapproveand a human: the approval policy actionsclose_periodandreopen_periodare denied to agents. - The entity is
CH01(Demo AG, CHF). January to August 2026 are closed and September is the first open period. The demo has foreign-currency invoices and a laptop that is being depreciated, so the checklist wants a depreciation run and an FX revaluation for September. - Earlier periods do not have to be closed first: an open earlier period is only a warning on the
checklist (
sequence). Here they are all closed.
1. Find the period
curl "$ERP_URL/v1/entities/CH01/periods?on=2026-09-30" \
-H "Authorization: Bearer $ERP_KEY"
[{"id": "<uuid>", "fiscal_year": 2026, "number": 9, "kind": "regular", "status": "open"}]
2. Read the close checklist
The checklist evaluates the conditions without changing anything. Each check has a status of
pass, fail, warn or n/a (not applicable). ready is true when no check fails; warnings do
not block.
curl "$ERP_URL/v1/periods/$PERIOD_ID/close-checklist" \
-H "Authorization: Bearer $ERP_KEY"
{
"entity": "CH01",
"period": "2026-09",
"period_status": "open",
"ready": false,
"checks": [
{"id": "approvals", "status": "pass"},
{"id": "drafts", "status": "pass"},
{"id": "depreciation", "status": "fail", "detail": "1 asset due; run depreciation"},
{"id": "fx_revaluation", "status": "fail", "detail": "run the FX revaluation"},
{"id": "deferred_tax", "status": "n/a", "detail": "due at year end"},
"..."
]
}
The thirteen checks, in the order of the answer:
| Id | What it checks | Fails when |
|---|---|---|
approvals |
No postings in the period wait for approval | one or more approval requests are pending |
drafts |
No unposted journal entries or documents | (a warning only) drafts exist |
depreciation |
The depreciation run was made | assets are due and the run is missing |
fx_revaluation |
The FX revaluation was made | there are foreign-currency documents or accounts and the run is missing |
deferred_tax |
The deferred-tax run was made | the tax book's cadence makes it due (period, or the last period of the year for year_end) and the run is missing |
ar_control_reconciled, ap_control_reconciled |
The receivables and payables control accounts equal their subledgers | there are postings on the control account that are not linked to a document |
opening_clearing |
The opening balance clearing account of a data import is zero (not applicable without an import) | it has a balance: the opening balances and the open items differ |
bank_reconciled |
Bank statement lines of the period are matched | there are unmatched statement lines |
bank_balances |
A statement's closing balance equals the ledger | a balance differs |
balanced |
The trial balance and the balance sheet balance | they do not |
integrity |
The hash chain of the ledger verifies | an entry was changed or is missing |
sequence |
Earlier periods are closed | (a warning only) an earlier period is open |
The demo has no bank statement for September, so the bank checks pass or do not apply.
3. Try to close
Closing evaluates the checklist and refuses when a check fails; the message names the failing checks.
curl -X POST "$ERP_URL/v1/periods/$PERIOD_ID/close" \
-H "Authorization: Bearer $ERP_KEY"
{"error": {"code": "period_not_ready", "message": "period 2026-09 cannot be closed: depreciation, fx_revaluation"}}
4. Run the depreciation
One entry dated the last day of the period, per asset: debit depreciation expense, credit accumulated depreciation. Only the laptop is depreciated in this demo. (How assets are registered and disposed of is the subject of Register an asset, depreciate it, dispose of it.)
curl -X POST "$ERP_URL/v1/periods/$PERIOD_ID/depreciation" \
-H "Authorization: Bearer $ERP_KEY"
{
"kind": "depreciation",
"period": "2026-09",
"items": [{"asset": "FA-001", "amount": "66.67"}],
"total": "66.67",
"submit": {"status": "posted"}
}
5. Run the FX revaluation
Open foreign-currency receivables and payables, and lease liabilities in a foreign currency, are
remeasured at the closing ECB rate of the period end. The unrealised difference is posted against the unrealised-FX account and reversed
automatically on the first day of the next period. items has one line per remeasured document
with its result (a gain positive, a loss negative: a EUR bill that grows in CHF is a loss), and
total is the result posted in CHF. A posting dated in the period after the run (a late payment,
a late invoice) makes the close check ask for the run again, which books only the change.
curl -X POST "$ERP_URL/v1/periods/$PERIOD_ID/fx-revaluation" \
-H "Authorization: Bearer $ERP_KEY"
{
"kind": "fx_revaluation",
"period": "2026-09",
"items": [
{"currency": "EUR", "open": "2000.0000", "carrying": "1887.6000", "closing": "1889.00", "difference": "1.4000", "result": "-1.4000"},
"..."
],
"total": "-65.86",
"submit": {"status": "posted"}
}
Both runs happen once per period, unless a late posting changes the revaluation. The checklist is now ready.
curl "$ERP_URL/v1/periods/$PERIOD_ID/close-checklist" \
-H "Authorization: Bearer $ERP_KEY"
{"period_status": "open", "ready": true}
6. A pending approval blocks the close
An agent books an accrual for September. The policy sends every manual entry of an agent to a
second person, so the entry is pending_approval and is not yet in the ledger.
curl -X POST "$ERP_URL/v1/api-keys" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "booking agent", "actor_type": "agent", "scopes": ["read", "draft", "post"]}'
{"key": "<any>", "actor_type": "agent", "scopes": ["read", "draft", "post"]}
curl -X POST "$ERP_URL/v1/journal-entries" \
-H "Authorization: Bearer $AGENT_KEY" \
-H "Idempotency-Key: close-agent-accrual" \
-H "Content-Type: application/json" \
-d '{
"entity": "CH01",
"entry_date": "2026-09-15",
"description": "Accrued audit fees September",
"lines": [
{"account": "6500", "debit": "500.00"},
{"account": "2300", "credit": "500.00"}
]
}'
{
"result": {
"status": "pending_approval",
"policy": {"decision": "require_approval", "rule_id": "agent_manual_journals"}
}
}
The approvals check now fails and the period cannot be closed.
curl "$ERP_URL/v1/periods/$PERIOD_ID/close-checklist" \
-H "Authorization: Bearer $ERP_KEY"
{
"ready": false,
"checks": [
{"id": "approvals", "status": "fail", "detail": "1 pending approval in the period"},
"..."
]
}
curl -X POST "$ERP_URL/v1/periods/$PERIOD_ID/close" \
-H "Authorization: Bearer $ERP_KEY"
{"error": {"code": "period_not_ready", "message": "period 2026-09 cannot be closed: approvals"}}
A person other than the agent approves the entry. It is posted into September, and the check passes. The first key may look at the request, but it belongs to nobody and cannot decide it.
curl "$ERP_URL/v1/approvals" \
-H "Authorization: Bearer $ERP_KEY"
[
{
"id": "<uuid>",
"object_type": "journal_entry",
"action": "post",
"status": "pending",
"requested_by": {"type": "agent", "name": "booking agent"}
}
]
curl -X POST "$ERP_URL/v1/approvals/$APPROVAL_ID/approve" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Content-Type: application/json" \
-d '{"comment": "Agreed to the engagement letter"}'
{
"error": {
"code": "forbidden",
"message": "a key the system issued (such as the organization's first admin key) belongs to no known person and cannot approve; sign in as a person, or use a key a person created"
}
}
Alice, a person, approves. (The agent's key was created with the first key, so it is a system key too; it and Alice are different people.)
curl -X POST "$ERP_URL/v1/approvals/$APPROVAL_ID/approve" \
-H "Authorization: Bearer $ALICE_KEY" \
-H "Content-Type: application/json" \
-d '{"comment": "Agreed to the engagement letter"}'
{"status": "approved", "decided_by": {"type": "human"}}
7. Close the period
Closing needs the approve scope and a human. An agent key is refused:
curl -X POST "$ERP_URL/v1/periods/$PERIOD_ID/close" \
-H "Authorization: Bearer $AGENT_KEY"
{"error": {"code": "missing_right"}}
A person closes the period. The answer is the checklist afterwards, with period_status closed.
curl -X POST "$ERP_URL/v1/periods/$PERIOD_ID/close" \
-H "Authorization: Bearer $ALICE_KEY"
{"period": "2026-09", "period_status": "closed", "ready": true}
8. The closed period refuses postings
curl -X POST "$ERP_URL/v1/journal-entries" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: close-late-rent" \
-H "Content-Type: application/json" \
-d '{
"entity": "CH01",
"entry_date": "2026-09-20",
"description": "Late rent",
"lines": [
{"account": "6000", "debit": "100.00"},
{"account": "1000", "credit": "100.00"}
]
}'
{"error": {"code": "period_closed", "message": "period 2026-09 is closed", "period": "2026-09"}}
The posting service checks the period of the entry date and the database refuses a posted entry in a closed period as well, so the rule holds for every posting path: invoices, payments and the month-end runs too.
9. Reopen the period
A controller with the approve scope can reopen a period (policy action reopen_period; agents
are denied). Reopening needs a reason, 3 to 1,000 characters, which is kept in the audit trail.
A reason that is blank is refused:
curl -X POST "$ERP_URL/v1/periods/$PERIOD_ID/reopen" \
-H "Authorization: Bearer $ALICE_KEY" \
-H "Content-Type: application/json" \
-d '{"reason": " "}'
{"error": {"code": "reopen_reason_missing", "message": "reopening a period needs a reason"}}
A body without a reason, or with one shorter than three characters, does not even parse: the answer
is the standard detail list of the request validation (see conventions).
curl -X POST "$ERP_URL/v1/periods/$PERIOD_ID/reopen" \
-H "Authorization: Bearer $ALICE_KEY" \
-H "Content-Type: application/json" \
-d '{"reason": "ab"}'
{"detail": ["<any>", "..."]}
With a reason the period is open again and postings are possible.
curl -X POST "$ERP_URL/v1/periods/$PERIOD_ID/reopen" \
-H "Authorization: Bearer $ALICE_KEY" \
-H "Content-Type: application/json" \
-d '{"reason": "Rent for September was invoiced late"}'
{"fiscal_year": 2026, "number": 9, "status": "open", "closed_at": null}
The audit trail has the event period.reopen with the reason and the actor. The newest event of
the period comes first:
curl "$ERP_URL/v1/audit-events?object_type=period&object_id=$PERIOD_ID&limit=1" \
-H "Authorization: Bearer $ERP_KEY"
[
{
"action": "period.reopen",
"object_type": "period",
"data": {"reason": "Rent for September was invoiced late", "fiscal_year": 2026, "number": 9}
}
]
curl -X POST "$ERP_URL/v1/journal-entries" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: close-late-rent-again" \
-H "Content-Type: application/json" \
-d '{
"entity": "CH01",
"entry_date": "2026-09-20",
"description": "Late rent",
"lines": [
{"account": "6000", "debit": "100.00"},
{"account": "1000", "credit": "100.00"}
]
}'
{"result": {"status": "posted"}}
Close it again. The checklist is evaluated again; the runs of the period are still on record.
curl -X POST "$ERP_URL/v1/periods/$PERIOD_ID/close" \
-H "Authorization: Bearer $ALICE_KEY"
{"period": "2026-09", "period_status": "closed", "ready": true}
10. The integrity evidence
Every posted entry holds a hash of its content and of the entry before it. The chain
verification recomputes all of them; valid is true and entries is the number of posted entries
checked. A changed or missing entry would show valid: false, with the first broken entry and
the reason. The integrity check of the checklist runs the same verification.
curl "$ERP_URL/v1/entities/CH01/chain-verification" \
-H "Authorization: Bearer $ERP_KEY"
{"entity": "CH01", "entries": "<any>", "valid": true, "first_invalid": null, "reason": null}
The trial balance at the period end, with debits equal to credits:
curl "$ERP_URL/v1/entities/CH01/reports/trial-balance?as_of=2026-09-30" \
-H "Authorization: Bearer $ERP_KEY"
{"entity": "CH01", "currency": "CHF", "as_of": "2026-09-30", "balanced": true}
What can go wrong
Closing a closed period is refused.
curl -X POST "$ERP_URL/v1/periods/$PERIOD_ID/close" \
-H "Authorization: Bearer $ALICE_KEY"
{"error": {"code": "period_already_closed", "message": "period 2026-09 is already closed"}}
A month-end run is made once per period, whether or not the period is open.
curl -X POST "$ERP_URL/v1/periods/$PERIOD_ID/fx-revaluation" \
-H "Authorization: Bearer $ERP_KEY"
{"error": {"code": "fx_revaluation_done"}}
An agent cannot reopen a period either:
curl -X POST "$ERP_URL/v1/periods/$PERIOD_ID/reopen" \
-H "Authorization: Bearer $AGENT_KEY" \
-H "Content-Type: application/json" \
-d '{"reason": "Rent for September was invoiced late"}'
{"error": {"code": "missing_right"}}
Reference
GET /v1/entities/{entity}/periodsGET /v1/periods/{period_id}/close-checklistPOST /v1/periods/{period_id}/depreciationPOST /v1/periods/{period_id}/fx-revaluationPOST /v1/periods/{period_id}/closePOST /v1/periods/{period_id}/reopenGET /v1/approvalsPOST /v1/approvals/{approval_id}/approveGET /v1/audit-eventsGET /v1/entities/{entity}/chain-verificationGET /v1/entities/{entity}/reports/trial-balance