gnubok

Fiscal periods

Period lifecycle — lock, close, year-end, opening balances, FX revaluation. Async via the operations substrate.

Endpoints


GET /api/v1/companies/:companyId/fiscal-periods {#get-fiscal-periods-list}

fiscal-periods.list · scope reports:read

List fiscal periods (räkenskapsår).

Returns every fiscal period for the company ordered by period_start DESC. is_closed=true means bokslut has been signed; locked_at non-null means writes are blocked at the DB-trigger level.

Use when: You need to find the active period before booking, build a year-selector UI, or audit the period-lock history.

Don't use for: Creating, locking, or closing periods — those land in Phase 4 (POST /fiscal-periods/{id}/lock, :close, :year-end). Use the dashboard or wait for Phase 4.

Pitfalls

  • previous_period_id chains the bokslut continuity (BFNAR 2013:2). A null value on a non-first period is a data-quality red flag.
  • A period can be locked but not closed (löpande bokföring of the new year while bokslut work continues on the prior year — see BFL 5 kap 2 § for the löpande bokföring deadline).
  • BFL 3 kap caps a single fiscal period at 18 months. First-year exceptions are allowed.

Risk: low · Idempotent: yes · Reversible: no · Dry-run supported: no

Example response

{
  "data": [
    {
      "id": "fp_2026",
      "name": "Räkenskapsår 2026",
      "period_start": "2026-01-01",
      "period_end": "2026-12-31",
      "is_closed": false,
      "locked_at": null
    }
  ],
  "meta": {
    "request_id": "req_…",
    "api_version": "2026-05-12"
  }
}

POST /api/v1/companies/:companyId/fiscal-periods/:id/close {#post-fiscal-periods-close}

fiscal-periods.close · scope bookkeeping:write

Close a fiscal period (IRREVERSIBLE per BFL 5 kap 8 §).

Sets is_closed=true + closed_at on the period. Pre-requisites: period must be locked (call /lock first) AND year-end closing must have been executed (call /year-end first). Sync. The DB blocks any subsequent JE inserts.

Use when: Final step in the year-end flow: lock → year-end → close. Closing freezes the period for BFL 7 kap retention.

Don't use for: Locking a period (use /lock). Running the year-end closing entry (use /year-end). UNDOING a close (not supported — irreversible).

Pitfalls

  • Idempotency-Key is mandatory.
  • IRREVERSIBLE. Once is_closed=true, the period is read-only forever (BFL 5 kap 8 § + 7 kap).
  • Pre-conditions: locked + closing_entry_id present. Otherwise the call returns CONFLICT.

Risk: high · Idempotent: yes · Reversible: no · Dry-run supported: no

Example response

{
  "data": {
    "id": "a8f1…",
    "is_closed": true,
    "closed_at": "2026-05-12T14:30:00Z"
  },
  "meta": {
    "request_id": "req_…",
    "api_version": "2026-05-12"
  }
}

POST /api/v1/companies/:companyId/fiscal-periods/:id/currency-revaluation {#post-fiscal-periods-currency-revaluation}

fiscal-periods.currency-revaluation · scope bookkeeping:write

Run FX revaluation for the fiscal period.

Re-rates open foreign-currency AR (1510) and AP (2440) at the closing date's Riksbanken rate and posts the SEK delta to 3960 (valutakursvinst) / 7960 (valutakursförlust). Returns 202 with operation_id. Idempotent per-period: the engine throws if a revaluation has already been posted for the same fiscal_period_id.

Use when: Before /year-end if your books have open foreign-currency receivables or payables. /year-end also runs this internally, so you only need to call it separately when you want the FX-only entry without the full closing.

Don't use for: Re-running on the same period (CURRENCY_REVALUATION_ALREADY_EXISTS). Revaluing a closed period (the trigger blocks JE writes to closed periods).

Pitfalls

  • Idempotency-Key is mandatory.
  • Engine returns null if no open foreign-currency items exist — the operation succeeds with result.revaluation_entry_id=null.
  • as_of_date defaults to period_end if omitted.

Risk: high · Idempotent: yes · Reversible: yes · Dry-run supported: no

Example response

{
  "data": {
    "operation_id": "0e9c…",
    "type": "fiscal_periods.currency_revaluation",
    "status": "succeeded",
    "poll_url": "/api/v1/operations/0e9c…",
    "webhook_event": "operation.completed"
  },
  "meta": {
    "request_id": "req_…",
    "api_version": "2026-05-12"
  }
}

POST /api/v1/companies/:companyId/fiscal-periods/:id/lock {#post-fiscal-periods-lock}

fiscal-periods.lock · scope bookkeeping:write

Lock a fiscal period (no new entries can be posted into it).

Sets locked_at on the period. Refuses if uncategorised business transactions remain in the period — they must be bokfört first. The DB trigger blocks JE inserts into locked periods; locking is the application-level pre-step before /close. Sync.

Use when: Finishing a period and you want to stop new postings. Step 1 of a three-step year-end flow: lock → year-end → close.

Don't use for: Locking an already-closed period (no-op). Bypassing the uncategorised-transactions guard — categorise or mark-private first.

Pitfalls

  • Idempotency-Key is mandatory.
  • A period with uncategorised business transactions cannot be locked; the response surfaces the count.
  • Locking is reversible until /close. The unlock endpoint is not in v1; use the dashboard.

Risk: high · Idempotent: yes · Reversible: yes · Dry-run supported: no

Example response

{
  "data": {
    "id": "a8f1…",
    "locked_at": "2026-05-12T14:00:00Z",
    "is_closed": false
  },
  "meta": {
    "request_id": "req_…",
    "api_version": "2026-05-12"
  }
}

POST /api/v1/companies/:companyId/fiscal-periods/:id/opening-balances {#post-fiscal-periods-opening-balances}

fiscal-periods.opening-balances · scope bookkeeping:write

Generate opening-balance verifikation for the next fiscal period.

Reads the closed period's trial balance, filters to BAS class 1–2 accounts with non-zero closing balance, and posts an opening verifikation (status=posted) onto the next_period_id. Sync. The path id is the CLOSED period; body.next_period_id is the target.

Use when: After /year-end + /close on a period, generate the IB into the next period so the new year starts with the correct balance sheet.

Don't use for: Posting opening balances on a manually-edited basis (use POST /journal-entries with source_type=manual). Re-running on the same target period (will produce duplicate IB entries).

Pitfalls

  • Idempotency-Key is mandatory.
  • next_period_id must reference the SAME company and must NOT already have an IB entry. The engine throws if it does.
  • Only class 1 (assets) and 2 (equity/liabilities) flow into the IB; class 3-8 are zeroed by the closing entry.

Risk: high · Idempotent: yes · Reversible: yes · Dry-run supported: no

Example request

{
  "next_period_id": "7b3a…"
}

Example response

{
  "data": {
    "opening_entry_id": "4d2a…",
    "voucher_series": "A",
    "voucher_number": 1,
    "next_period_id": "7b3a…"
  },
  "meta": {
    "request_id": "req_…",
    "api_version": "2026-05-12"
  }
}

POST /api/v1/companies/:companyId/fiscal-periods/:id/year-end {#post-fiscal-periods-year-end}

fiscal-periods.year-end · scope bookkeeping:write

Execute year-end closing (currency revaluation + closing entry).

Async-operation endpoint. Runs the year-end closing flow: currency revaluation (FX gains/losses to 3960/7960), then posts the closing entry that zeroes class 3-8 onto årets resultat (2099 for AB, the relevant eget-kapital account in the 2010-2019 range for enskild firma — the engine resolves which based on company.entity_type). Returns 202 with operation_id; subscribe to operation.completed or poll /v1/operations/{id}.

Use when: After /lock and a passing /compliance/check?type=year_end_readiness, you want to run the closing entry. This is step 2 of the lock → year-end → close flow.

Don't use for: Re-running year-end (per-period idempotent — fails if closing_entry_id is already set). Closing the period (use /close after year-end succeeds).

Pitfalls

  • Idempotency-Key is mandatory.
  • Period must pass year_end_readiness checks (no drafts, no unexplained voucher gaps, trial balance balanced). The engine re-validates and aborts if not.
  • Closing entry is itself a verifikation (posted) — the period must NOT already be closed.

Risk: high · Idempotent: yes · Reversible: no · Dry-run supported: no

Example response

{
  "data": {
    "operation_id": "0e9c…",
    "type": "fiscal_periods.year_end",
    "status": "succeeded",
    "poll_url": "/api/v1/operations/0e9c…",
    "webhook_event": "operation.completed"
  },
  "meta": {
    "request_id": "req_…",
    "api_version": "2026-05-12"
  }
}