Journal entries
The bookkeeping engine surface — verifikation lifecycle (draft, commit, reverse, correct).
Endpoints
GET/api/v1/companies/:companyId/journal-entries— List journal entries (verifikationer).GET/api/v1/companies/:companyId/journal-entries/:id— Retrieve a single verifikation by id.POST/api/v1/companies/:companyId/journal-entries— Create a draft journal entry (verifikation).POST/api/v1/companies/:companyId/journal-entries/:id/commit— Commit a draft journal entry.POST/api/v1/companies/:companyId/journal-entries/:id/correct— Correct a posted journal entry (BFL 5:5 storno-then-replace).POST/api/v1/companies/:companyId/journal-entries/:id/reverse— Storno a posted journal entry.POST/api/v1/companies/:companyId/journal-entries/batch-create— Create up to 50 draft journal entries (partial-success).
GET /api/v1/companies/:companyId/journal-entries {#get-journal-entries-list}
journal-entries.list · scope reports:read
List journal entries (verifikationer).
Cursor-paginated list of journal entries. Filters: fiscal_period_id, status, date_from, date_to. Excludes status=cancelled by default; pass status=cancelled to inspect storno-cancelled drafts.
Use when: You need to walk the verifikationsserie for a period (audit, SIE export, gap detection) or list recent activity for a UI.
Don't use for: Reading a single verifikation (use GET /{id}). Reading lines without the header (no separate endpoint — they ride in /{id}).
Pitfalls
- Cancelled drafts are hidden by default. They are NOT a löpnummer gap (no voucher_number is allocated for drafts); the filter is for noise reduction.
- voucher_number=0 indicates a draft that has not been committed. Posted entries always have voucher_number > 0.
Risk: low · Idempotent: yes · Reversible: no · Dry-run supported: no
Example response
{
"data": [
{
"id": "0e9c…",
"fiscal_period_id": "a8f1…",
"voucher_series": "A",
"voucher_number": 142,
"entry_date": "2026-05-12",
"description": "Levfaktura 2026-1234, Office Depot AB (ankomst 42)",
"status": "posted",
"source_type": "supplier_invoice_registered",
"created_at": "2026-05-13T15:00:00Z"
}
],
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12",
"next_cursor": null
}
}
GET /api/v1/companies/:companyId/journal-entries/:id {#get-journal-entries-get}
journal-entries.get · scope reports:read
Retrieve a single verifikation by id.
Returns the full journal entry including all lines, dimensions, and the storno chain (reverses_id, reversed_by_id, correction_of_id).
Use when: You need the full verifikation for audit / reconciliation, or to display the line-by-line breakdown.
Don't use for: Listing entries (use the list endpoint with filters).
Pitfalls
- Cancelled drafts are returned (no filter on status here); inspect status before assuming the entry is posted.
- Lines are sorted by sort_order; the order matters for display but not for accounting (the sum across lines is the meaningful quantity).
Risk: low · Idempotent: yes · Reversible: no · Dry-run supported: no
Example response
{
"data": {
"id": "0e9c…",
"voucher_series": "A",
"voucher_number": 142,
"entry_date": "2026-05-12",
"status": "posted",
"lines": [
{
"account_number": "6570",
"debit_amount": 50,
"credit_amount": 0,
"sort_order": 0
},
{
"account_number": "1930",
"debit_amount": 0,
"credit_amount": 50,
"sort_order": 1
}
]
},
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12"
}
}
POST /api/v1/companies/:companyId/journal-entries {#post-journal-entries-create-draft}
journal-entries.create-draft · scope bookkeeping:write
Create a draft journal entry (verifikation).
Creates a draft journal entry via the engine's createDraftEntry(). The draft has no voucher_number until /commit is called. Idempotent (mandatory Idempotency-Key). Dry-runnable: a dry-run validates balance + account-chart membership + period date constraints without inserting any row.
Use when: You're posting an arbitrary verifikation — manual journal entries, accrual reversals, period closing adjustments — outside the invoicing / supplier-invoice / transaction flows.
Don't use for: Bookkeeping flows that have a dedicated endpoint (invoices, supplier-invoices, transactions). Editing an existing posted entry — use /correct instead.
Pitfalls
- Idempotency-Key is mandatory.
- Lines must sum to zero (Σ debit = Σ credit). Engine rejects with JOURNAL_ENTRY_NOT_BALANCED on imbalance.
- entry_date must fall within fiscal_period_id's [period_start, period_end]; otherwise ENTRY_DATE_OUTSIDE_FISCAL_PERIOD.
- All account_numbers must be active in the chart_of_accounts; otherwise ACCOUNTS_NOT_IN_CHART.
- voucher_series defaults to "A" if omitted. Must be a single uppercase letter.
- This creates a DRAFT only — call POST /{id}/commit to assign the voucher_number and post atomically.
Risk: high · Idempotent: yes · Reversible: yes · Dry-run supported: yes
Example request
{
"fiscal_period_id": "a8f1…",
"entry_date": "2026-05-12",
"description": "Bankavgift maj 2026",
"lines": [
{
"account_number": "6570",
"debit_amount": 50,
"credit_amount": 0,
"line_description": "Bankavgift"
},
{
"account_number": "1930",
"debit_amount": 0,
"credit_amount": 50,
"line_description": "Företagskonto"
}
]
}
Example response
{
"data": {
"id": "0e9c…",
"status": "draft",
"voucher_series": "A",
"voucher_number": 0
},
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12"
}
}
POST /api/v1/companies/:companyId/journal-entries/:id/commit {#post-journal-entries-commit}
journal-entries.commit · scope bookkeeping:write
Commit a draft journal entry.
Atomically advances the voucher series and flips the draft to posted. The voucher_number is the smallest integer not yet used in (fiscal_period_id, voucher_series); a failed commit does NOT burn the number.
Use when: You created a draft via POST /journal-entries and now want to post it to the books. After commit the entry is immutable per BFL 5 kap 2 §; corrections require /reverse or /correct.
Don't use for: Re-committing an already-posted entry (returns 409). Committing across companies — the URL companyId must match the draft's company.
Pitfalls
- Idempotency-Key is mandatory.
- Posted entries cannot be edited. Plan the lines carefully or call /correct after commit if you need to change them.
- Voucher numbers are sequential within (fiscal_period_id, voucher_series). A commit failure (e.g. period locked between draft creation and commit) does not advance the sequence.
Risk: high · Idempotent: yes · Reversible: yes · Dry-run supported: yes
Example response
{
"data": {
"id": "0e9c…",
"voucher_series": "A",
"voucher_number": 143,
"status": "posted",
"entry_date": "2026-05-12"
},
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12"
}
}
POST /api/v1/companies/:companyId/journal-entries/:id/correct {#post-journal-entries-correct}
journal-entries.correct · scope bookkeeping:write
Correct a posted journal entry (BFL 5:5 storno-then-replace).
Per Bokföringslagen 5 kap 5 §, posted entries cannot be modified. This endpoint creates the canonical correction trail: a storno reversing the original, then a new entry with the corrected lines. All three are visible in the verifikationsserie and linked via reverses_id / reversed_by_id / correction_of_id. Idempotent. Dry-runnable.
Use when: You need to amend a posted verifikation. Use this rather than /reverse when the entry is being REPLACED with new lines — /reverse just nullifies.
Don't use for: Drafts (no voucher_number — cancel via dashboard). Already-corrected entries (the chain only supports one correction; correct the latest in the chain).
Pitfalls
- Idempotency-Key is mandatory.
- The new lines must balance. JOURNAL_ENTRY_NOT_BALANCED if not.
- The original's entry_date and fiscal_period_id are inherited. If the original's period has been locked since posting, the call returns PERIOD_LOCKED.
- Three voucher numbers are advanced in this call: the original (already burned), the reversal, and the corrected. The series stays unbroken.
Risk: high · Idempotent: yes · Reversible: no · Dry-run supported: yes
Example request
{
"lines": [
{
"account_number": "6570",
"debit_amount": 75,
"credit_amount": 0,
"line_description": "Bankavgift (rättad)"
},
{
"account_number": "1930",
"debit_amount": 0,
"credit_amount": 75,
"line_description": "Företagskonto"
}
]
}
Example response
{
"data": {
"reversal_id": "4d2a…",
"corrected_id": "7b3a…",
"original_id": "0e9c…",
"voucher_series": "A",
"reversal_voucher_number": 144,
"corrected_voucher_number": 145
},
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12"
}
}
POST /api/v1/companies/:companyId/journal-entries/:id/reverse {#post-journal-entries-reverse}
journal-entries.reverse · scope bookkeeping:write
Storno a posted journal entry.
Creates a reversing journal entry that nullifies the original. The original remains posted and visible — the reversal links via reverses_id and the original is annotated reversed_by_id. The reversal carries its own voucher_number in the same series so the löpnummer chain stays unbroken (BFL 5 kap 5–7 §§).
Use when: A posted entry needs to be cancelled and there is no replacement coming — e.g. a duplicate booking, an entry posted to the wrong period. Use /correct instead when you need to replace the entry with corrected lines.
Don't use for: Cancelling a draft (drafts have no voucher_number; cancel via the dashboard). Reversing an already-reversed entry (returns ENTRY_ALREADY_REVERSED).
Pitfalls
- Idempotency-Key is mandatory.
- reversal_date defaults to today; the reversal is posted in the fiscal period covering that date. If today's period is locked the call returns PERIOD_LOCKED.
- You cannot reverse a draft (status must be posted). Use /correct after commit if the original needs replacing.
Risk: high · Idempotent: yes · Reversible: no · Dry-run supported: yes
Example request
{
"reversal_date": "2026-05-13"
}
Example response
{
"data": {
"reversal_id": "4d2a…",
"original_id": "0e9c…",
"voucher_series": "A",
"voucher_number": 144,
"entry_date": "2026-05-13",
"status": "posted"
},
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12"
}
}
POST /api/v1/companies/:companyId/journal-entries/batch-create {#post-journal-entries-batch-create}
journal-entries.batch-create · scope bookkeeping:write
Create up to 50 draft journal entries (partial-success).
Bulk-create endpoint mirroring /invoices/bulk-create and /suppliers/bulk-create. Each entry is validated and inserted independently — per-item failures do not roll back items that succeeded. Returns DRAFTS only; commit each separately. Idempotent over the whole batch. Dry-runnable.
Use when: You're replaying historical bookkeeping from another system, or batching a set of manual verifikationer from a spreadsheet. Use dry-run first to validate the batch.
Don't use for: Committing posted entries — use POST /{id}/commit per entry. Transactional all-or-nothing imports — passing all_or_nothing: true returns 501 NOT_IMPLEMENTED.
Pitfalls
- Idempotency-Key is mandatory and covers the WHOLE batch.
- all_or_nothing: true returns 501 NOT_IMPLEMENTED. Today only partial-success batches exist.
- Each entry must balance independently. Per-item JOURNAL_ENTRY_NOT_BALANCED appears in the results array.
Risk: high · Idempotent: yes · Reversible: yes · Dry-run supported: yes
Example request
{
"journal_entries": [
{
"fiscal_period_id": "a8f1…",
"entry_date": "2026-05-12",
"description": "Bankavgift",
"lines": [
{
"account_number": "6570",
"debit_amount": 50,
"credit_amount": 0
},
{
"account_number": "1930",
"debit_amount": 0,
"credit_amount": 50
}
]
}
]
}
Example response
{
"data": {
"results": [
{
"ok": true,
"request_index": 0,
"data": {
"id": "0e9c…",
"status": "draft"
}
}
],
"summary": {
"total": 1,
"succeeded": 1,
"failed": 0
}
},
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12"
}
}