StabisoDocs

REST API

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_KEY is 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 need draft, and closing or reopening needs approve and a human: the approval policy actions close_period and reopen_period are 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

Stabiso, the Enterprise System of Record · generated from the code and its guides · release d6b82bcf059d