{"openapi":"3.1.0","info":{"title":"Accounted API","version":"2026-05-12","description":"Public REST API for Accounted: Swedish double-entry bookkeeping. Every write supports dry-run via `?dry_run=true`. Every request must include `Authorization: Bearer gnubok_sk_...`. See /docs/api for the cookbook."},"servers":[{"url":"https://app.gnubok.se"}],"components":{"securitySchemes":{"ApiKey":{"type":"http","scheme":"bearer","bearerFormat":"gnubok_sk_<live|test>_<random>"}}},"security":[{"ApiKey":[]}],"paths":{"/api/v1/health":{"get":{"operationId":"health.check","summary":"Health check.","description":"Reports the API is reachable and what version is currently served. Public; no auth required.\n\n**Use when:** You want to verify connectivity, latency, or which API version is live before issuing other requests.\n**Do not use for:** Anything that needs authenticated data. This endpoint returns no company-specific information.\n\n**Pitfalls:**\n- A 200 here only means the API process responds: downstream Postgres/Supabase may still be degraded.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"status":{"type":"string","enum":["ok","degraded"]},"service":{"const":"gnubok"},"api_version":{"type":"string"},"timestamp":{"type":"string"}},"required":["status","service","api_version","timestamp"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies":{"get":{"operationId":"companies.list","summary":"List companies the API key can access.","description":"Returns every non-archived company the API key user is a member of, together with their role. Use the returned `id` as `{companyId}` in subsequent endpoints.\n\n**Use when:** You need to discover which company IDs an API key has access to before calling company-scoped endpoints.\n**Do not use for:** Fetching a single company you already know the id of: use GET /api/v1/companies/{companyId} for that.\n\n**Pitfalls:**\n- Multi-company keys (e.g. consultants) will see >1 result. Always pass the correct companyId in subsequent paths.\n- Archived companies are excluded; if a company disappears the user has been removed from it or it was archived.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"companies:read","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"org_number":{"type":"string"},"entity_type":{"type":"string"},"role":{"type":"string","enum":["owner","admin","member","viewer"]},"created_at":{"type":"string"}},"required":["id","name","org_number","entity_type","role","created_at"],"additionalProperties":false}},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/operations/{id}":{"get":{"operationId":"operations.get","summary":"Poll a long-running operation by id.","description":"Returns the current snapshot of a v1 async operation: status (queued / running / succeeded / failed / cancelled), progress (jsonb, free-form), result (on success), and error (on failure). The operation_id is returned by the POST endpoints that initiate async work (period close, year-end, currency revaluation, SIE import).\n\n**Use when:** You started an async operation and need to know whether it has finished. Poll every 5-30 seconds until a terminal status. (The 202 response advertises `operation.completed` as the eventual push signal, but that webhook event is not deliverable yet — polling is the only supported completion signal today.)\n**Do not use for:** Fetching the resource the operation produced: once status=succeeded, read the result field or call the resource-specific GET endpoint. Cancelling a running operation (no cancel endpoint exists in v1).\n\n**Pitfalls:**\n- Terminal statuses (`succeeded`, `failed`, `cancelled`) are final; the row never transitions out of them.\n- progress is free-form jsonb; agents should treat it as opaque except for the documented fields `phase` (string), `current` / `total` (numbers for percent calculation).\n- started_at is null while status=queued (the work has not begun yet); completed_at is null until a terminal status is reached.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"operations:read","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"operation_id":{"type":"string"},"type":{"type":"string"},"status":{"type":"string","enum":["queued","running","succeeded","failed","cancelled"]},"progress":{},"result":{},"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"additionalProperties":false},"started_at":{"type":"string"},"completed_at":{"type":"string"},"poll_url":{"type":"string"},"webhook_event":{"const":"operation.completed"}},"required":["operation_id","type","status","result","error","started_at","completed_at","poll_url","webhook_event"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/journal-entries":{"get":{"operationId":"journal-entries.list","summary":"List journal entries (verifikationer).","description":"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.\n\n**Use when:** You need to walk the verifikationsserie for a period (audit, SIE export, gap detection) or list recent activity for a UI.\n**Do not use for:** Reading a single verifikation (use GET /{id}). Reading lines without the header (no separate endpoint: they ride in /{id}).\n\n**Pitfalls:**\n- 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.\n- voucher_number=0 indicates a draft that has not been committed. Posted entries always have voucher_number > 0.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"reports:read","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"fiscal_period_id":{"type":"string"},"voucher_series":{"type":"string"},"voucher_number":{"type":"number"},"entry_date":{"type":"string"},"description":{"type":"string"},"status":{"type":"string","enum":["draft","posted","cancelled"]},"source_type":{"type":"string"},"created_at":{"type":"string"}},"required":["id","fiscal_period_id","voucher_series","voucher_number","entry_date","description","status","source_type","created_at"],"additionalProperties":false}},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}},"post":{"operationId":"journal-entries.create-draft","summary":"Create a draft journal entry (verifikation).","description":"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.\n\n**Use when:** You're posting an arbitrary verifikation (manual journal entries, accrual reversals, period closing adjustments) outside the invoicing / supplier-invoice / transaction flows.\n**Do not use for:** Bookkeeping flows that have a dedicated endpoint (invoices, supplier-invoices, transactions). Editing an existing posted entry: use /correct instead.\n\n**Pitfalls:**\n- Idempotency-Key is mandatory.\n- Lines must sum to zero (Σ debit = Σ credit). Engine rejects with JOURNAL_ENTRY_NOT_BALANCED on imbalance.\n- entry_date must fall within fiscal_period_id's [period_start, period_end]; otherwise ENTRY_DATE_OUTSIDE_FISCAL_PERIOD.\n- All account_numbers must be active in the chart_of_accounts; otherwise ACCOUNTS_NOT_IN_CHART.\n- voucher_series defaults to \"A\" if omitted. Must be a single uppercase letter.\n- This creates a DRAFT only: call POST /{id}/commit to assign the voucher_number and post atomically.","x-action-risk":"high","x-idempotent":true,"x-reversible":true,"x-dry-run-supported":true,"x-required-scope":"bookkeeping:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"fiscal_period_id":{"type":"string"},"voucher_series":{"type":"string"},"voucher_number":{"type":"number"},"entry_date":{"type":"string"},"description":{"type":"string"},"status":{"type":"string","enum":["draft","posted","cancelled"]},"source_type":{"type":"string"},"created_at":{"type":"string"},"notes":{"type":"string"},"reverses_id":{"type":"string"},"reversed_by_id":{"type":"string"},"correction_of_id":{"type":"string"},"lines":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"account_number":{"type":"string"},"debit_amount":{"type":"number"},"credit_amount":{"type":"number"},"line_description":{"type":"string"},"currency":{"type":"string"},"amount_in_currency":{"type":"number"},"exchange_rate":{"type":"number"},"tax_code":{"type":"string"},"cost_center":{"type":"string"},"project":{"type":"string"}},"required":["id","account_number","debit_amount","credit_amount","line_description","currency","amount_in_currency","exchange_rate","tax_code","cost_center","project"],"additionalProperties":false}}},"required":["id","fiscal_period_id","voucher_series","voucher_number","entry_date","description","status","source_type","created_at","notes","reverses_id","reversed_by_id","correction_of_id","lines"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/journal-entries/{id}":{"get":{"operationId":"journal-entries.get","summary":"Retrieve a single verifikation by id.","description":"Returns the full journal entry including all lines, dimensions, and the storno chain (reverses_id, reversed_by_id, correction_of_id).\n\n**Use when:** You need the full verifikation for audit / reconciliation, or to display the line-by-line breakdown.\n**Do not use for:** Listing entries (use the list endpoint with filters).\n\n**Pitfalls:**\n- Cancelled drafts are returned (no filter on status here); inspect status before assuming the entry is posted.\n- Lines are sorted by sort_order; the order matters for display but not for accounting (the sum across lines is the meaningful quantity).","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"reports:read","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"fiscal_period_id":{"type":"string"},"voucher_series":{"type":"string"},"voucher_number":{"type":"number"},"entry_date":{"type":"string"},"description":{"type":"string"},"status":{"type":"string","enum":["draft","posted","cancelled"]},"source_type":{"type":"string"},"source_id":{"type":"string"},"notes":{"type":"string"},"reverses_id":{"type":"string"},"reversed_by_id":{"type":"string"},"correction_of_id":{"type":"string"},"lines":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"account_number":{"type":"string"},"debit_amount":{"type":"number"},"credit_amount":{"type":"number"},"line_description":{"type":"string"},"currency":{"type":"string"},"amount_in_currency":{"type":"number"},"exchange_rate":{"type":"number"},"tax_code":{"type":"string"},"cost_center":{"type":"string"},"project":{"type":"string"},"sort_order":{"type":"number"}},"required":["id","account_number","debit_amount","credit_amount","line_description","currency","amount_in_currency","exchange_rate","tax_code","cost_center","project","sort_order"],"additionalProperties":false}},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","fiscal_period_id","voucher_series","voucher_number","entry_date","description","status","source_type","source_id","notes","reverses_id","reversed_by_id","correction_of_id","lines","created_at","updated_at"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/journal-entries/{id}/commit":{"post":{"operationId":"journal-entries.commit","summary":"Commit a draft journal entry.","description":"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.\n\n**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.\n**Do not use for:** Re-committing an already-posted entry (returns 409). Committing across companies: the URL companyId must match the draft's company.\n\n**Pitfalls:**\n- Idempotency-Key is mandatory.\n- Posted entries cannot be edited. Plan the lines carefully or call /correct after commit if you need to change them.\n- 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.","x-action-risk":"high","x-idempotent":true,"x-reversible":true,"x-dry-run-supported":true,"x-required-scope":"bookkeeping:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"voucher_series":{"type":"string"},"voucher_number":{"type":"number"},"status":{"const":"posted"},"entry_date":{"type":"string"}},"required":["id","voucher_series","voucher_number","status","entry_date"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/journal-entries/{id}/reverse":{"post":{"operationId":"journal-entries.reverse","summary":"Storno a posted journal entry.","description":"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 §§).\n\n**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.\n**Do not use for:** Cancelling a draft (drafts have no voucher_number; cancel via the dashboard). Reversing an already-reversed entry (returns ENTRY_ALREADY_REVERSED).\n\n**Pitfalls:**\n- Idempotency-Key is mandatory.\n- 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.\n- You cannot reverse a draft (status must be posted). Use /correct after commit if the original needs replacing.","x-action-risk":"high","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":true,"x-required-scope":"bookkeeping:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"reversal_id":{"type":"string"},"original_id":{"type":"string"},"voucher_series":{"type":"string"},"voucher_number":{"type":"number"},"entry_date":{"type":"string"},"status":{"const":"posted"}},"required":["reversal_id","original_id","voucher_series","voucher_number","entry_date","status"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/journal-entries/{id}/correct":{"post":{"operationId":"journal-entries.correct","summary":"Correct a posted journal entry (BFL 5:5 storno-then-replace).","description":"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.\n\n**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.\n**Do not use for:** Drafts (no voucher_number: cancel via dashboard). Already-corrected entries (the chain only supports one correction; correct the latest in the chain).\n\n**Pitfalls:**\n- Idempotency-Key is mandatory.\n- The new lines must balance. JOURNAL_ENTRY_NOT_BALANCED if not.\n- 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.\n- Three voucher numbers are advanced in this call: the original (already burned), the reversal, and the corrected. The series stays unbroken.","x-action-risk":"high","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":true,"x-required-scope":"bookkeeping:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"reversal_id":{"type":"string"},"corrected_id":{"type":"string"},"original_id":{"type":"string"},"voucher_series":{"type":"string"},"reversal_voucher_number":{"type":"number"},"corrected_voucher_number":{"type":"number"}},"required":["reversal_id","corrected_id","original_id","voucher_series","reversal_voucher_number","corrected_voucher_number"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/journal-entries/batch-create":{"post":{"operationId":"journal-entries.batch-create","summary":"Create up to 50 draft journal entries (partial-success).","description":"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.\n\n**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.\n**Do not 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.\n\n**Pitfalls:**\n- Idempotency-Key is mandatory and covers the WHOLE batch.\n- all_or_nothing: true returns 501 NOT_IMPLEMENTED. Today only partial-success batches exist.\n- Each entry must balance independently. Per-item JOURNAL_ENTRY_NOT_BALANCED appears in the results array.","x-action-risk":"high","x-idempotent":true,"x-reversible":true,"x-dry-run-supported":true,"x-required-scope":"bookkeeping:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object","properties":{"ok":{"type":"boolean"},"request_index":{"type":"number"},"data":{},"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["ok","request_index"],"additionalProperties":false}},"summary":{"type":"object","properties":{"total":{"type":"number"},"succeeded":{"type":"number"},"failed":{"type":"number"}},"required":["total","succeeded","failed"],"additionalProperties":false}},"required":["results","summary"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/voucher-gap-explanations":{"post":{"operationId":"voucher-gap-explanations.create","summary":"Document a gap in the verifikationsserie (BFL 5 kap 6-7 §§).","description":"Records an explanation for one or more missing voucher numbers in a series. Required when a number is unaccounted for during audit. Statutory basis: BFL 5 kap 6-7 §§ (verifikationsnummer i löpande följd utan luckor); BFNAR 2013:2 kap 8 § governs the systemdokumentation that surfaces the gap. Idempotent. Dry-runnable.\n\n**Use when:** You're responding to a voucher-gap audit finding and need to document the cause. Also used by migration flows that claim numbers without filling them.\n**Do not use for:** Falsifying a series: every gap MUST have a genuine explanation. The dashboard surfaces these for auditor review.\n\n**Pitfalls:**\n- Idempotency-Key is mandatory.\n- gap_end must be >= gap_start; a single-number gap has gap_start = gap_end.\n- voucher_series is a single uppercase letter (A-Z); the same series + period + numeric range must not already exist.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":true,"x-required-scope":"bookkeeping:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"fiscal_period_id":{"type":"string"},"voucher_series":{"type":"string"},"gap_start":{"type":"number"},"gap_end":{"type":"number"},"explanation":{"type":"string"},"created_at":{"type":"string"}},"required":["id","fiscal_period_id","voucher_series","gap_start","gap_end","explanation","created_at"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/compliance/check":{"get":{"operationId":"compliance.check","summary":"Run a structured compliance pre-flight check.","description":"Generalised pre-flight that consolidates the Accounted pre-close validators under one envelope. Supported check types: year_end_readiness (BFNAR 2017:3 + ÅRL 2:1 blockers), voucher_gaps (BFNAR 2013:2 kap 8 § series continuity). vat_close is planned for a follow-up PR (the underlying function currently lives in the MCP extension and core routes cannot import from extensions; it will be extracted into lib/reports/ then exposed here). New types can be added without changing the response shape.\n\n**Use when:** Before committing to an irreversible action (VAT close, year-end close), or as a periodic audit sweep to surface blockers before they become urgent.\n**Do not use for:** Executing the underlying action: this is read-only. After a passing check, call the corresponding async endpoint (POST /fiscal-periods/{id}/year-end, etc).\n\n**Pitfalls:**\n- year_end_readiness and voucher_gaps require fiscal_period_id (UUID).\n- A passing check is a SNAPSHOT: the state can change between the check and the action. The same blocker logic runs again on commit.\n- vat_close is documented in the plan but NOT yet supported by this endpoint: call gnubok_vat_close_check via the MCP server until the function is extracted into lib/reports/.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"compliance:read","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"type":{"type":"string"},"ready":{"type":"boolean"},"findings":{"type":"array","items":{"type":"object","properties":{"severity":{"type":"string","enum":["info","warning","blocker"]},"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["severity","code","message"],"additionalProperties":false}},"summary":{"type":"string"},"generated_at":{"type":"string"},"params":{}},"required":["type","ready","findings","summary","generated_at","params"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/fiscal-periods/{id}/lock":{"post":{"operationId":"fiscal-periods.lock","summary":"Lock a fiscal period (no new entries can be posted into it).","description":"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.\n\n**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.\n**Do not use for:** Locking an already-closed period (no-op). Bypassing the uncategorised-transactions guard: categorise or mark-private first.\n\n**Pitfalls:**\n- Idempotency-Key is mandatory.\n- A period with uncategorised business transactions cannot be locked; the response surfaces the count.\n- Locking is reversible until /close. The unlock endpoint is not in v1; use the dashboard.","x-action-risk":"high","x-idempotent":true,"x-reversible":true,"x-dry-run-supported":false,"x-required-scope":"bookkeeping:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"locked_at":{"type":"string"},"is_closed":{"type":"boolean"}},"required":["id","locked_at","is_closed"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/fiscal-periods/{id}/close":{"post":{"operationId":"fiscal-periods.close","summary":"Close a fiscal period (IRREVERSIBLE per BFL 5 kap 8 §).","description":"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.\n\n**Use when:** Final step in the year-end flow: lock → year-end → close. Closing freezes the period for BFL 7 kap retention.\n**Do not use for:** Locking a period (use /lock). Running the year-end closing entry (use /year-end). UNDOING a close (not supported, irreversible).\n\n**Pitfalls:**\n- Idempotency-Key is mandatory.\n- IRREVERSIBLE. Once is_closed=true, the period is read-only forever (BFL 5 kap 8 § + 7 kap).\n- Pre-conditions: locked + closing_entry_id present. Otherwise the call returns CONFLICT.","x-action-risk":"high","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"bookkeeping:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"is_closed":{"const":true},"closed_at":{"type":"string"}},"required":["id","is_closed","closed_at"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/fiscal-periods/{id}/year-end":{"post":{"operationId":"fiscal-periods.year-end","summary":"Execute year-end closing (currency revaluation + closing entry).","description":"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}.\n\n**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.\n**Do not 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).\n\n**Pitfalls:**\n- Idempotency-Key is mandatory.\n- Period must pass year_end_readiness checks (no drafts, no unexplained voucher gaps, trial balance balanced). The engine re-validates and aborts if not.\n- Closing entry is itself a verifikation (posted): the period must NOT already be closed.","x-action-risk":"high","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"bookkeeping:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"operation_id":{"type":"string"},"type":{"const":"fiscal_periods.year_end"},"status":{"type":"string","enum":["queued","running","succeeded","failed"]},"poll_url":{"type":"string"},"webhook_event":{"const":"operation.completed"}},"required":["operation_id","type","status","poll_url","webhook_event"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/fiscal-periods/{id}/opening-balances":{"post":{"operationId":"fiscal-periods.opening-balances","summary":"Generate opening-balance verifikation for the next fiscal period.","description":"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.\n\n**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.\n**Do not 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).\n\n**Pitfalls:**\n- Idempotency-Key is mandatory.\n- next_period_id must reference the SAME company and must NOT already have an IB entry. The engine throws if it does.\n- Only class 1 (assets) and 2 (equity/liabilities) flow into the IB; class 3-8 are zeroed by the closing entry.","x-action-risk":"high","x-idempotent":true,"x-reversible":true,"x-dry-run-supported":false,"x-required-scope":"bookkeeping:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"opening_entry_id":{"type":"string"},"voucher_series":{"type":"string"},"voucher_number":{"type":"number"},"next_period_id":{"type":"string"}},"required":["opening_entry_id","voucher_series","voucher_number","next_period_id"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/fiscal-periods/{id}/currency-revaluation":{"post":{"operationId":"fiscal-periods.currency-revaluation","summary":"Run FX revaluation for the fiscal period.","description":"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.\n\n**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.\n**Do not use for:** Re-running on the same period (CURRENCY_REVALUATION_ALREADY_EXISTS). Revaluing a closed period (the trigger blocks JE writes to closed periods).\n\n**Pitfalls:**\n- Idempotency-Key is mandatory.\n- Engine returns null if no open foreign-currency items exist: the operation succeeds with result.revaluation_entry_id=null.\n- as_of_date defaults to period_end if omitted.","x-action-risk":"high","x-idempotent":true,"x-reversible":true,"x-dry-run-supported":false,"x-required-scope":"bookkeeping:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"operation_id":{"type":"string"},"type":{"const":"fiscal_periods.currency_revaluation"},"status":{"type":"string","enum":["queued","running","succeeded","failed"]},"poll_url":{"type":"string"},"webhook_event":{"const":"operation.completed"}},"required":["operation_id","type","status","poll_url","webhook_event"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/documents":{"post":{"operationId":"documents.upload","summary":"Upload a document to the WORM archive.","description":"Multipart upload of a document (PDF / image) under the BFL 7 kap retention regime. The bytes are hashed (SHA-256), written to Supabase Storage, and recorded in document_attachments at version=1. Allowed MIME types: application/pdf, image/jpeg, image/png, image/webp. Max size: 10 MB.\n\n**Use when:** You have a receipt, invoice scan, or supporting document for a posted verifikation and want it archived for the 7-year BFL retention period. Optionally link to a journal entry at upload time via journal_entry_id.\n**Do not use for:** Updating an existing document (no v1 update endpoint; new versions go through the dashboard). Bulk uploads: call once per file.\n\n**Pitfalls:**\n- Idempotency-Key is mandatory; multipart retries with the same key replay the cached response.\n- Max size 10 MB enforced server-side: DOC_UPLOAD_TOO_LARGE on overrun.\n- Only application/pdf / image/jpeg / image/png / image/webp accepted: DOC_UPLOAD_UNSUPPORTED_TYPE otherwise.\n- WORM: once linked to a posted journal entry, the document row cannot be modified or deleted (DB trigger). Upload-then-link is reversible (the document exists with journal_entry_id=null until linked); once linked, treat as immutable.\n- Dry-run is not supported on this endpoint: the engine hashes + stores + inserts in one atomic flow.","x-action-risk":"medium","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"documents:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"file_name":{"type":"string"},"mime_type":{"type":"string"},"file_size_bytes":{"type":"number"},"sha256_hash":{"type":"string"},"version":{"type":"number"},"is_current_version":{"type":"boolean"},"upload_source":{"type":"string"},"journal_entry_id":{"type":"string"},"journal_entry_line_id":{"type":"string"},"created_at":{"type":"string"}},"required":["id","file_name","mime_type","file_size_bytes","sha256_hash","version","is_current_version","upload_source","journal_entry_id","journal_entry_line_id","created_at"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/documents/{id}/download":{"get":{"operationId":"documents.download","summary":"Get a time-limited signed download URL for a document.","description":"Returns a Supabase Storage signed URL valid for 15 minutes. The URL itself is the canonical download: fetch it with any HTTP client; no API key needed on the storage host. Verify file integrity client-side against the returned sha256_hash if your workflow requires it.\n\n**Use when:** You need the bytes of an archived document (e.g. for OCR, attachment to an email, regulatory export). Always re-fetch the URL before each download: old URLs expire.\n**Do not use for:** Persisting the URL anywhere: it expires. Storing the URL in a webhook payload or audit log makes the audit trail dependent on URL state.\n\n**Pitfalls:**\n- The signed URL expires after 15 minutes. Don't cache it beyond the immediate transaction.\n- The URL leaks the Supabase Storage origin; this is benign (the signature alone authorizes the read) but rate-limit any forwarding so you don't reveal the storage layout to untrusted callers.\n- Each call emits a document.accessed event. Polling this endpoint produces audit noise; cache the URL for its full TTL.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"documents:read","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"file_name":{"type":"string"},"mime_type":{"type":"string"},"sha256_hash":{"type":"string"},"is_current_version":{"type":"boolean"},"download_url":{"type":"string"},"expires_in_seconds":{"type":"number"}},"required":["id","file_name","mime_type","sha256_hash","is_current_version","download_url","expires_in_seconds"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/documents/{id}/link":{"post":{"operationId":"documents.link","summary":"Link a document to a journal entry.","description":"Sets journal_entry_id (and optionally journal_entry_line_id) on an existing document. Optionally stamps the originating invoice_inbox_items row as consumed via inbox_item_id. Use this after /documents upload when the link target was unknown at upload time, or to re-link a stray document. Once the target JE is posted, the document row is effectively immutable per BFL 7 kap retention.\n\n**Use when:** A document was uploaded without a journal_entry_id (e.g. bulk import) and you now want to attach it to a posted verifikation. Pass inbox_item_id when the document came from the invoice inbox so the item is marked resolved in one call.\n**Do not use for:** Unlinking: no v1 unlink endpoint. The dashboard exposes a manual override; v1 keeps the WORM contract by refusing to revert posted-JE links.\n\n**Pitfalls:**\n- Idempotency-Key is mandatory.\n- Both the document and the journal_entry_id must belong to the caller's company. NOT_FOUND on mismatch (enumeration hardening).\n- Re-linking an already-linked document overwrites the previous journal_entry_id: confirm the old target is what you intend to break.\n- inbox_item_id stamping is best-effort: if the stamp fails the document link still succeeds. Use POST /api/v1/companies/:companyId/inbox-items/:id/stamp to stamp independently.","x-action-risk":"medium","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":true,"x-required-scope":"documents:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"journal_entry_id":{"type":"string"},"journal_entry_line_id":{"type":"string"},"file_name":{"type":"string"}},"required":["id","journal_entry_id","journal_entry_line_id","file_name"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/invoices":{"get":{"operationId":"invoices.list","summary":"List invoices for a company.","description":"Returns invoices in most-recent-first order. Includes the customer name inline; pass ?expand=customer for the full customer record, ?expand=items for line items.\n\n**Use when:** You need to enumerate invoices for a company: for AR reporting, payment matching, or building an invoice dashboard.\n**Do not use for:** Fetching a single invoice you already know the id of: use GET /api/v1/companies/{companyId}/invoices/{id}. Supplier invoices are a different resource (supplier-invoices).\n\n**Pitfalls:**\n- Draft invoices have invoice_number=null until they are sent.\n- remaining_amount is the unpaid portion (total − paid_amount); use status=paid or remaining_amount=0 to filter for closed invoices.\n- Credit notes appear with status=credited and a credited_invoice_id field on the detail endpoint.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"invoices:read","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"invoice_number":{"type":"string"},"customer_id":{"type":"string"},"customer_name":{"type":"string"},"invoice_date":{"type":"string"},"due_date":{"type":"string"},"status":{"type":"string","enum":["draft","sent","paid","partially_paid","overdue","cancelled","credited"]},"document_type":{"type":"string","enum":["invoice","proforma","delivery_note"]},"currency":{"type":"string"},"subtotal":{"type":"number"},"vat_amount":{"type":"number"},"total":{"type":"number"},"remaining_amount":{"type":"number"},"paid_at":{"type":"string"},"created_at":{"type":"string"}},"required":["id","invoice_number","customer_id","customer_name","invoice_date","due_date","status","document_type","currency","subtotal","vat_amount","total","remaining_amount","paid_at","created_at"],"additionalProperties":false}},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}},"post":{"operationId":"invoices.create","summary":"Create a draft invoice, proforma, or delivery note.","description":"Creates an invoice in draft status. The F-series invoice_number is allocated atomically on the first send action (PR-B-2b). Per-item VAT rates are validated against the customer's allowed rates (mixed-rate invoices supported). Non-SEK invoices are converted to SEK at the Riksbanken exchange rate fetched at create time. Supports ROT/RUT deduction lines (items[].deduction_type = \"rot\"|\"rut\" with invoice-level deduction_personnummer + deduction_housing_designation, or deduction_apartment_number + deduction_brf_org_number for bostadsrätt), article linkage (items[].article_id + optional revenue_account override from the artikelregister), and project/cost-centre tagging (default_dimensions / items[].dimensions). Idempotent (mandatory Idempotency-Key). Dry-runnable: the preview returns the validated would-be invoice + items with computed totals; no journal entry is involved at draft stage (posting happens on :send). Set is_self_billed=true (with external_invoice_number + received_date) to instead register a received self-billing invoice (mottagen självfaktura, ML 17 kap 15§): a sale booked immediately with the counterparty's number, not a draft.\n\n**Use when:** You need to issue a new invoice, proforma, or delivery note. Use dry-run first to confirm VAT calculations and currency conversion before committing.\n**Do not use for:** Updating an existing invoice (PATCH instead, drafts only). Issuing a credit note (use POST /:id:credit in PR-B-2b). Posting a previously-created draft to the journal (use POST /:id:send in PR-B-2b).\n\n**Pitfalls:**\n- Idempotency-Key is mandatory; calls without it return 400.\n- For mixed-rate invoices, set vat_rate per item explicitly. Items where vat_rate is omitted use the customer's default rate from getVatRules().\n- Non-SEK currencies require an active Riksbanken exchange-rate fetch. Failure is non-fatal: the invoice is created with null SEK fields and the agent can recompute later.\n- invoice_number is null on creation. The number is allocated atomically when the invoice transitions out of draft. Counting on a specific number at create time is a bug.\n- document_type='delivery_note' produces no VAT and a different number sequence (D-series). Most use cases want the default document_type='invoice'.\n- is_self_billed=true registers a self-billing invoice your CUSTOMER issued on your behalf (a sale for you). It is booked immediately (not a draft, no F-number), so external_invoice_number and received_date are required and it is NOT dry-run-free of side effects on the live call. Do NOT set it for a normal invoice you issue yourself.\n- Project/cost-center tagging: pass default_dimensions ({\"6\":\"P001\"} = project, {\"1\":\"KS01\"} = kostnadsställe) for the whole invoice and/or items[].dimensions per line (per-line wins per key). Tags are stored on the draft and applied to the journal entry lines when the invoice is sent. When the company has the dimension registry enabled, unknown or archived codes are rejected at :send with 400 DIMENSION_VALIDATION_FAILED — list valid codes via GET /dimensions.\n- ROT/RUT: set items[].deduction_type (\"rot\"|\"rut\") on labor lines plus labor_hours and work_type (Skatteverket arbetstypskod). The invoice must carry deduction_personnummer AND housing info: deduction_housing_designation (fastighetsbeteckning) for småhus, or deduction_apartment_number + deduction_brf_org_number for bostadsrätt. deduction_amount is computed server-side and cannot be set by the caller; the response exposes deduction_total and remaining_amount = total - deduction_total (Skatteverket pays the rest via 1513). Validation failures return 400 INVOICE_CREATE_ROT_RUT_VALIDATION.\n- Articles: pass items[].article_id (from the artikelregister, GET /articles) to link a line to a catalog article; price/description are still taken from the request body (the API never auto-fills from the article: send the values you want on the invoice). items[].revenue_account optionally overrides the BAS class-3 account and is validated against the chart of accounts.","x-action-risk":"medium","x-idempotent":true,"x-reversible":true,"x-dry-run-supported":true,"x-required-scope":"invoices:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"invoice_number":{"type":"string"},"customer_id":{"type":"string"},"invoice_date":{"type":"string"},"due_date":{"type":"string"},"status":{"type":"string"},"document_type":{"type":"string"},"currency":{"type":"string"},"subtotal":{"type":"number"},"vat_amount":{"type":"number"},"total":{"type":"number"},"remaining_amount":{"type":"number"},"created_at":{"type":"string"}},"required":["id","invoice_number","customer_id","invoice_date","due_date","status","document_type","currency","subtotal","vat_amount","total","remaining_amount","created_at"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/invoices/{id}":{"get":{"operationId":"invoices.get","summary":"Retrieve a single invoice by id.","description":"Returns the full invoice record with the customer embedded. Pass ?expand=items for line items, ?expand=payments for payment history, or ?expand=items,payments for both.\n\n**Use when:** You have an invoice id (from a webhook, the list endpoint, or a customer transaction) and need the full record including amounts, dates, status, and the customer details.\n**Do not use for:** Listing invoices (use GET /api/v1/companies/{companyId}/invoices). Bookkeeping verifikationer tied to the invoice (use the journal-entries endpoints in a later phase).\n\n**Pitfalls:**\n- Returns 404 if the invoice does not belong to the company in the URL: does not leak existence across companies.\n- paid_at and remaining_amount can lag behind the latest payment by a few seconds during high-volume reconciliation.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"invoices:read","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"invoice_number":{"type":"string"},"customer_id":{"type":"string"},"invoice_date":{"type":"string"},"due_date":{"type":"string"},"status":{"type":"string"},"document_type":{"type":"string"},"currency":{"type":"string"},"total":{"type":"number"},"remaining_amount":{"type":"number"},"paid_at":{"type":"string"},"created_at":{"type":"string"}},"required":["id","invoice_number","customer_id","invoice_date","due_date","status","document_type","currency","total","remaining_amount","paid_at","created_at"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}},"patch":{"operationId":"invoices.update","summary":"Update a draft invoice (metadata fields only).","description":"Partial update for invoices in draft status. Allowed fields: invoice_date, due_date, delivery_date, your_reference, our_reference, notes, default_dimensions (project/cost-centre tags, e.g. {\"6\":\"P001\"}; replaces the whole bag). customer_id, currency, document_type, items, and computed totals are immutable: replace those by deleting the draft and recreating it. Returns 409 INVOICE_UPDATE_NOT_DRAFT if the invoice is no longer in draft status. Idempotent and dry-runnable.\n\n**Use when:** You need to correct a typo, push the due date, or update a customer reference on a draft you have not sent yet. The invoice number stays null until the first :send action.\n**Do not use for:** Updating a sent / paid / credited invoice (those are immutable per ML 17 kap; issue a credit note via POST /:id:credit in PR-B-2b). Changing items, currency, or customer: drafts are cheap to delete and recreate.\n\n**Pitfalls:**\n- Idempotency-Key is mandatory.\n- A 409 INVOICE_UPDATE_NOT_DRAFT means the invoice has been sent / paid / credited / cancelled. The error code name is shared with the DELETE handler.\n- Items are immutable here: to change line items, delete the draft and POST a fresh one.\n- default_dimensions replaces the entire bag (no per-key merge): read the current value first if you want to add a tag. Send {} to clear all tags. Codes are validated against the dimension registry at :send, not at PATCH time.","x-action-risk":"low","x-idempotent":true,"x-reversible":true,"x-dry-run-supported":true,"x-required-scope":"invoices:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"invoice_number":{"type":"string"},"customer_id":{"type":"string"},"invoice_date":{"type":"string"},"due_date":{"type":"string"},"status":{"type":"string"},"document_type":{"type":"string"},"currency":{"type":"string"},"total":{"type":"number"},"remaining_amount":{"type":"number"},"paid_at":{"type":"string"},"created_at":{"type":"string"}},"required":["id","invoice_number","customer_id","invoice_date","due_date","status","document_type","currency","total","remaining_amount","paid_at","created_at"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/customers":{"get":{"operationId":"customers.list","summary":"List customers for a company.","description":"Returns active customers in created-first order. Pass ?include_archived=true to include archived rows. Use ?search to match against name or org_number.\n\n**Use when:** You need a customer roster: for building a UI picker, syncing a CRM, or resolving a customer_id before creating an invoice.\n**Do not use for:** Fetching a single customer you already know the id of: use GET /api/v1/companies/{companyId}/customers/{id}. Suppliers are a separate resource.\n\n**Pitfalls:**\n- Archived customers are hidden by default; the dashboard makes the same choice.\n- org_number is included so callers can match against external CRM identifiers; for sole traders (enskild firma) it equals the personnummer.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"customers:read","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"customer_type":{"type":"string","enum":["individual","swedish_business","eu_business","non_eu_business"]},"email":{"type":"string"},"org_number":{"type":"string"},"vat_number":{"type":"string"},"default_payment_terms":{"type":"number"},"archived_at":{"type":"string"},"created_at":{"type":"string"}},"required":["id","name","customer_type","email","org_number","vat_number","default_payment_terms","archived_at","created_at"],"additionalProperties":false}},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}},"post":{"operationId":"customers.create","summary":"Create a customer.","description":"Creates a new customer for the company. Requires Idempotency-Key (UUID). Supports ?dry_run=true for input validation without committing: the dry-run response shows the would-be record minus id and timestamps. EU-business customers with a VAT number are auto-validated against VIES on commit.\n\n**Use when:** You need to register a new customer before invoicing them. Use dry-run first to catch validation errors before committing.\n**Do not use for:** Updating an existing customer (PATCH instead). Creating suppliers (different resource).\n\n**Pitfalls:**\n- Idempotency-Key is mandatory: calls without it return 400 VALIDATION_ERROR.\n- org_number uniqueness is enforced at the database level; duplicate inserts return 409 CUSTOMER_DUPLICATE_ORG_NUMBER.\n- For Swedish sole traders (customer_type=individual), org_number IS the personnummer. List responses mask it; the create endpoint accepts it as input.\n- VIES validation runs only on commit. Dry-run skips the external call and leaves vat_number_validated=false in the preview.","x-action-risk":"low","x-idempotent":true,"x-reversible":true,"x-dry-run-supported":true,"x-required-scope":"customers:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"customer_type":{"type":"string","enum":["individual","swedish_business","eu_business","non_eu_business"]},"customer_number":{"type":"string"},"email":{"type":"string"},"phone":{"type":"string"},"address_line1":{"type":"string"},"address_line2":{"type":"string"},"postal_code":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"org_number":{"type":"string"},"vat_number":{"type":"string"},"vat_number_validated":{"type":"boolean"},"default_payment_terms":{"type":"number"},"notes":{"type":"string"},"archived_at":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","name","customer_type","customer_number","email","phone","address_line1","address_line2","postal_code","city","country","org_number","vat_number","vat_number_validated","default_payment_terms","notes","archived_at","created_at","updated_at"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/customers/{id}":{"get":{"operationId":"customers.get","summary":"Retrieve a single customer by id.","description":"Returns the full customer record. Pass ?expand=invoices to embed any open invoices (sent / partially_paid / overdue) for the customer in the same response.\n\n**Use when:** You need the full customer record: address, payment terms, VAT validation status, contact details: before invoicing or syncing to another system.\n**Do not use for:** Listing customers (use the list endpoint). Looking up arbitrary supplier or employee records (different resources).\n\n**Pitfalls:**\n- archived_at is non-null when the customer has been soft-deleted; the customer is still queryable by id but excluded from default lists.\n- vat_number_validated reflects the last successful VIES check; it can become stale if the EU registry revokes a number.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"customers:read","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"customer_type":{"type":"string"},"customer_number":{"type":"string"},"email":{"type":"string"},"phone":{"type":"string"},"address_line1":{"type":"string"},"address_line2":{"type":"string"},"postal_code":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"org_number":{"type":"string"},"vat_number":{"type":"string"},"vat_number_validated":{"type":"boolean"},"default_payment_terms":{"type":"number"},"notes":{"type":"string"},"archived_at":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","name","customer_type","customer_number","email","phone","address_line1","address_line2","postal_code","city","country","org_number","vat_number","vat_number_validated","default_payment_terms","notes","archived_at","created_at","updated_at"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}},"patch":{"operationId":"customers.update","summary":"Partially update a customer.","description":"Patches the customer with the supplied fields. All fields optional. Idempotent (mandatory Idempotency-Key). Dry-runnable. When vat_number changes on an eu_business customer, VIES re-validation runs on commit (best-effort).\n\n**Use when:** You need to change a customer's contact details, payment terms, address, or VAT registration. Use dry-run first to confirm the merged record before committing.\n**Do not use for:** Archiving a customer (use DELETE: sets archived_at). Replacing the entire record (no PUT verb is exposed; PATCH is partial).\n\n**Pitfalls:**\n- Idempotency-Key is mandatory; calls without it return 400.\n- org_number uniqueness is enforced at DB level: 23505 → 409 CUSTOMER_DUPLICATE_ORG_NUMBER.\n- VIES re-validation is best-effort and runs only on commit. A VIES timeout does not fail the update.","x-action-risk":"low","x-idempotent":true,"x-reversible":true,"x-dry-run-supported":true,"x-required-scope":"customers:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"customer_type":{"type":"string"},"customer_number":{"type":"string"},"email":{"type":"string"},"phone":{"type":"string"},"address_line1":{"type":"string"},"address_line2":{"type":"string"},"postal_code":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"org_number":{"type":"string"},"vat_number":{"type":"string"},"vat_number_validated":{"type":"boolean"},"default_payment_terms":{"type":"number"},"notes":{"type":"string"},"archived_at":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","name","customer_type","customer_number","email","phone","address_line1","address_line2","postal_code","city","country","org_number","vat_number","vat_number_validated","default_payment_terms","notes","archived_at","created_at","updated_at"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}},"delete":{"operationId":"customers.delete","summary":"Archive a customer (soft-delete).","description":"Sets archived_at on the customer; the record is preserved (invoices and audit history remain intact) but excluded from default list responses. To un-archive, PATCH archived_at back to null. Idempotent: archiving an already-archived customer is a no-op. Dry-runnable.\n\n**Use when:** You want to remove a customer from active rosters without losing their history. Idempotent: re-archiving is safe.\n**Do not use for:** Permanently deleting a customer with all history: the public API does not expose hard-delete. GDPR erasure requests go through a dedicated workflow.\n\n**Pitfalls:**\n- Idempotency-Key is mandatory.\n- A customer with any open invoice (sent / partially_paid / overdue) cannot be archived: returns 409 CUSTOMER_HAS_INVOICES. Issue a kreditfaktura first if you need to close the relationship cleanly. This protects ML 17 kap 24§: the customer record is the canonical source of buyer name/address for invoice reissuance.\n- 204 No Content is returned on success: there is no response body to parse.","x-action-risk":"medium","x-idempotent":true,"x-reversible":true,"x-dry-run-supported":true,"x-required-scope":"customers:write","responses":{"204":{"description":"No Content"},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/invoices/{id}/mark-sent":{"post":{"operationId":"invoices.mark-sent","summary":"Transition a draft invoice to sent (without emailing).","description":"Marks a draft invoice as sent: for invoices delivered outside Accounted (Peppol, postal, manual email). Allocates the F-series invoice_number atomically (ML 17 kap 24§ p.2). On accounting_method=accrual, also posts the invoice journal entry (Debit AR 1510 / Credit revenue + output VAT). Emits invoice.sent. Idempotent and dry-runnable. The companion :send action (PR-B-2b-3) adds PDF rendering and email delivery on top of this same flow.\n\n**Use when:** You delivered the invoice through a channel other than Accounted's email (Peppol, postal, your own SMTP) and need to record it as sent so the F-series number is allocated and the journal entry is posted.\n**Do not use for:** Sending the invoice via Accounted email: use :send (PR-B-2b-3) for that. Marking an already-sent invoice as paid: use :mark-paid (PR-B-2b-2).\n\n**Pitfalls:**\n- Only invoices in `status=draft` can be marked sent. Other states return 409 INVOICE_UPDATE_NOT_DRAFT (re-used; the action is structurally an update).\n- Allocation is atomic. If a concurrent transition beats the agent's request to the same draft, the runner-up gets 409 INVOICE_UPDATE_NOT_DRAFT and no number is consumed.\n- Delivery notes (document_type=delivery_note) don't transition to sent: they were never drafts in the f-series sense. This endpoint will reject them with 400 VALIDATION_ERROR.\n- Idempotency-Key is mandatory. A retried mark-sent with the same key replays the cached response.","x-action-risk":"medium","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":true,"x-required-scope":"invoices:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"invoice_number":{"type":"string"},"status":{"const":"sent"},"total":{"type":"number"},"journal_entry_id":{"type":"string"},"warnings":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}}},"required":["id","invoice_number","status","total","journal_entry_id"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/invoices/{id}/mark-paid":{"post":{"operationId":"invoices.mark-paid","summary":"Record a payment against an invoice.","description":"Marks a sent / overdue invoice as paid (or partially_paid). Books the payment via Debit 1930 / Credit 1510 under faktureringsmetoden, or Debit 1930 / Credit revenue + Credit output VAT under kontantmetoden. Optional body supports partial payments via custom balanced journal lines and exchange-rate adjustments for foreign-currency invoices. Idempotent and dry-runnable. Emits invoice.paid.\n\n**Use when:** A customer paid an invoice via a channel other than the synced bank account (cash, manual transfer, separate processor). Use dry-run to confirm the booking before committing.\n**Do not use for:** Reverting a payment: the public API does not expose unmark-paid. Issue a credit note via POST /:id/credit to cancel the underlying invoice instead. Bank-matched payments: those flow through the transactions endpoints.\n\n**Pitfalls:**\n- Idempotency-Key is mandatory. Retried marks with the same key replay the cached response.\n- Custom `lines` must balance (sum of debits = sum of credits, both > 0). Otherwise returns 400 INVOICE_PAID_LINES_UNBALANCED.\n- For foreign-currency invoices, supply `exchange_rate_difference` (SEK delta vs the invoice's booked rate) to book the FX adjustment correctly. Omitting it on a non-SEK invoice will mis-book the FX gain/loss.\n- Cash basis (kontantmetoden) recognizes revenue HERE, not at :mark-sent. The dashboard tracks this via company_settings.accounting_method.\n- Duplicate-payment guard: if an unlinked inbound bank transaction looks like this payment, returns 409 INVOICE_PAID_LIKELY_DUPLICATE with candidate transactions. Retry with `force: true` to bypass, but the retry MUST use a fresh Idempotency-Key (the original is body-hash bound; reusing it returns 400 IDEMPOTENCY_KEY_REUSE). The guard is also evaluated under dry-run, so a successful dry-run does not guarantee a successful commit.","x-action-risk":"medium","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":true,"x-required-scope":"invoices:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"invoice_number":{"type":"string"},"status":{"type":"string","enum":["paid","partially_paid"]},"total":{"type":"number"},"paid_amount":{"type":"number"},"remaining_amount":{"type":"number"},"paid_at":{"type":"string"},"journal_entry_id":{"type":"string"},"warnings":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}}},"required":["id","invoice_number","status","total","paid_amount","remaining_amount","paid_at","journal_entry_id"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/invoices/{id}/credit":{"post":{"operationId":"invoices.credit","summary":"Issue a credit note (kreditfaktura) against an invoice.","description":"Creates a credit note referencing the original invoice. The credit note carries reversed-sign amounts (matching the original line for line) and gets invoice_number=KR-<original>. The original invoice transitions to status=credited. Under faktureringsmetoden, posts a reversing journal entry (Credit AR 1510 / Debit revenue + Debit output VAT). Under kontantmetoden the credit note still creates the row but defers the reversal entry until refund. Idempotent and dry-runnable. Emits invoice.credited.\n\n**Use when:** You need to legally cancel an issued invoice (ML 17 kap 22-23§). The original invoice cannot be edited once issued: credit it and reissue corrected.\n**Do not use for:** Cancelling a draft (DELETE the draft instead). Refunding a partial payment without invalidating the whole invoice (book the refund manually via the journal-entries API in a future PR).\n\n**Pitfalls:**\n- Idempotency-Key is mandatory. Retried credits with the same key replay the cached response: no duplicate credit note is created.\n- The original invoice must be in sent / paid / overdue status. Drafts, cancelled invoices, and already-credited invoices are rejected with specific error codes.\n- Credit-note items mirror the original's lines with negated values. To credit only part of an invoice (line-level), credit the full invoice first then reissue with the corrected lines.\n- Under kontantmetoden no journal entry is created here: refund booking is deferred. A `JOURNAL_ENTRY_NOT_POSTED` warning is NOT emitted in this case (the deferral is correct, not a failure).","x-action-risk":"high","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":true,"x-required-scope":"invoices:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"invoice_number":{"type":"string"},"credited_invoice_id":{"type":"string"},"status":{"const":"sent"},"total":{"type":"number"},"journal_entry_id":{"type":"string"},"warnings":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}}},"required":["id","invoice_number","credited_invoice_id","status","total","journal_entry_id"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/invoices/{id}/send":{"post":{"operationId":"invoices.send","summary":"Send a draft invoice to the customer by email.","description":"The full send pipeline: preflight PDF render → allocate F-series number atomically → final PDF render → email via Resend (PDF attachment, copy to company) → flip status to sent → post journal entry (accrual + real invoice) → archive PDF as underlag → emit invoice.sent. Email failure is a hard 502 before state changes; post-email failures surface as warnings but the invoice IS marked sent.\n\n**Use when:** You want Accounted to deliver the invoice to the customer via email. For invoices delivered through another channel (Peppol, postal, own SMTP) use :mark-sent instead.\n**Do not use for:** Re-sending an already-sent invoice (returns 409 INVOICE_UPDATE_NOT_DRAFT). Sending a delivery note (no F-series lifecycle). Sending a credit note (use the :credit endpoint to issue the kreditfaktura; subsequent re-send of the credit note via :mark-sent is the supported path).\n\n**Pitfalls:**\n- Idempotency-Key is mandatory.\n- Email service must be configured: without RESEND_API_KEY + RESEND_FROM_EMAIL the endpoint returns 503 INVOICE_SEND_EMAIL_NOT_CONFIGURED.\n- Customer must have an email address. 400 INVOICE_SEND_NO_CUSTOMER_EMAIL otherwise.\n- A cancelled invoice is rejected (400 INVOICE_SEND_CANCELLED): its F-series number is preserved for compliance but the document is not a valid faktura.\n- Email failure before the status flip leaves the F-series number consumed but the invoice in `draft` status. Same orphan window as :mark-sent (architecturally tracked, matches internal route).\n- After the email succeeds, journal-entry/archive/event failures become warnings on the response; the invoice IS marked sent regardless.","x-action-risk":"high","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":true,"x-required-scope":"invoices:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"invoice_number":{"type":"string"},"status":{"const":"sent"},"total":{"type":"number"},"message_id":{"type":"string"},"sent_to":{"type":"string"},"cc":{"type":"string"},"journal_entry_id":{"type":"string"},"warnings":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false}}},"required":["id","invoice_number","status","total","message_id","sent_to","cc","journal_entry_id"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/invoices/bulk-create":{"post":{"operationId":"invoices.bulk-create","summary":"Create up to 50 draft invoices in one call (partial-success).","description":"Bulk-creation endpoint. Each invoice in the request array is validated and inserted independently. By default, individual failures do not roll back successes: the response carries a per-item results array with ok/error markers and a summary. Idempotent (the whole batch is keyed by the single Idempotency-Key). Dry-runnable.\n\n**Use when:** You're importing a batch of invoices from another system, or producing many invoices programmatically (e.g. monthly subscription billing). Use dry-run first to validate the whole batch before committing.\n**Do not use for:** Sending the same invoice to multiple customers: POST /invoices once per customer. Long-running imports of > 50 invoices: split into pages. Transactional all-or-nothing imports: not yet supported (passing all_or_nothing: true returns 501 NOT_IMPLEMENTED; the flag is reserved for a future RPC).\n\n**Pitfalls:**\n- Idempotency-Key is mandatory and covers the WHOLE batch. A retried bulk-create returns the cached full response: it does not retry only the failed items.\n- Passing all_or_nothing: true returns 501 NOT_IMPLEMENTED. Today only partial-success batches exist; omit the flag (or pass false).\n- Each per-item invoice still goes through the same VAT-rule validation as POST /invoices. A mismatched per-item vat_rate produces a per-item failure, not a whole-batch failure.\n- Currency conversion is best-effort PER ITEM. A failed Riksbanken fetch leaves that item's SEK columns null but does NOT fail the item.","x-action-risk":"medium","x-idempotent":true,"x-reversible":true,"x-dry-run-supported":true,"x-required-scope":"invoices:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object","properties":{"ok":{"type":"boolean"},"request_index":{"type":"number"},"data":{},"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["ok","request_index"],"additionalProperties":false}},"summary":{"type":"object","properties":{"total":{"type":"number"},"succeeded":{"type":"number"},"failed":{"type":"number"}},"required":["total","succeeded","failed"],"additionalProperties":false}},"required":["results","summary"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/invoices/{id}/pdf":{"get":{"operationId":"invoices.pdf","summary":"Download the rendered invoice PDF.","description":"Returns the invoice as application/pdf. The filename in Content-Disposition reflects the document type: faktura-<number>.pdf for sent invoices, kreditfaktura-<number>.pdf for credit notes, utkast-<id-slice>.pdf for drafts. This endpoint is byte-equivalent to the dashboard download.\n\n**Use when:** You need to fetch an invoice PDF for archival, forwarding to a customer outside the Accounted send flow, or attaching to an external workflow.\n**Do not use for:** Sending the invoice to the customer: use POST /invoices/{id}/send, which renders the PDF, emails it, and archives it as a verifikationsunderlag in one atomic step.\n\n**Pitfalls:**\n- Drafts (no invoice_number yet) render with an \"utkast\" filename. The PDF carries no F-series number: do not treat it as a finalized invoice.\n- PDF rendering can take several hundred milliseconds for invoices with many line items. Cache on the client if requesting repeatedly.\n- Credit notes embed the original invoice's löpnummer per ML 17 kap 22-23§: if the original was hard-deleted (not possible via Accounted but theoretically via a manual DB edit), the reference is omitted.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"invoices:read","responses":{"200":{"description":"Success","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/customers/bulk-create":{"post":{"operationId":"customers.bulk-create","summary":"Create up to 50 customers in one call (partial-success).","description":"Bulk-create endpoint mirroring /invoices/bulk-create. Each customer is validated and inserted independently: per-item failures do not roll back items that succeeded. Returns a results array plus a summary. Idempotent over the whole batch. Dry-runnable.\n\n**Use when:** You're importing a roster of customers from another CRM, or seeding a fresh company with its existing client list. Use dry-run first to validate the batch.\n**Do not use for:** Updating existing customers: PATCH /customers/{id} once per customer. Bulk uploads of > 50 customers: split into pages of 50. Transactional all-or-nothing imports: passing all_or_nothing: true returns 501 NOT_IMPLEMENTED.\n\n**Pitfalls:**\n- Idempotency-Key is mandatory and covers the WHOLE batch. A retried bulk-create returns the cached full response: it does not retry only the failed items.\n- Passing all_or_nothing: true returns 501 NOT_IMPLEMENTED. Today only partial-success batches exist; omit the flag or pass false.\n- org_number uniqueness is enforced at the DB level: items with duplicates fail individually with CUSTOMER_DUPLICATE_ORG_NUMBER.\n- VIES validation for eu_business customers is best-effort per item; a VIES timeout leaves vat_number_validated=false but does NOT fail the item.","x-action-risk":"low","x-idempotent":true,"x-reversible":true,"x-dry-run-supported":true,"x-required-scope":"customers:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object","properties":{"ok":{"type":"boolean"},"request_index":{"type":"number"},"data":{},"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["ok","request_index"],"additionalProperties":false}},"summary":{"type":"object","properties":{"total":{"type":"number"},"succeeded":{"type":"number"},"failed":{"type":"number"}},"required":["total","succeeded","failed"],"additionalProperties":false}},"required":["results","summary"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/transactions":{"get":{"operationId":"transactions.list","summary":"List transactions for a company.","description":"Cursor-paginated transaction list ordered by created_at DESC, id ASC (newest-imported first; the `date` column is the transaction date and is filterable but not the sort key). Filter by ?status=booked|unbooked, ?currency, ?date_from / ?date_to, ?search (description ilike).\n\n**Use when:** You need to walk a company's bank ledger: building a categorization queue, reconciling against external statements, or sampling for audit.\n**Do not use for:** Looking up one transaction by id (use the detail endpoint). Reconciliation status (use /reconciliation/bank/status).\n\n**Pitfalls:**\n- Default page size is 50. Pass ?limit=100 for the maximum. Cursor pagination: pass ?cursor=<next_cursor> from the previous response.\n- A booked transaction has a non-null journal_entry_id. is_business / category live on the transaction row even before booking.\n- reverse-charge or storno entries can leave a transaction with journal_entry_id pointing at a cancelled JE: check status on the JE separately.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"transactions:read","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"date":{"type":"string"},"description":{"type":"string"},"amount":{"type":"number"},"currency":{"type":"string"},"reference":{"type":"string"},"merchant_name":{"type":"string"},"journal_entry_id":{"type":"string"},"invoice_id":{"type":"string"},"supplier_invoice_id":{"type":"string"},"is_business":{"type":"boolean"},"category":{"type":"string"},"import_source":{"type":"string"},"created_at":{"type":"string"}},"required":["id","date","description","amount","currency","reference","merchant_name","journal_entry_id","invoice_id","supplier_invoice_id","is_business","category","import_source","created_at"],"additionalProperties":false}},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/transactions/{id}":{"get":{"operationId":"transactions.get","summary":"Retrieve a single transaction by id.","description":"Returns the full transaction record including match state, booking state, and import metadata.\n\n**Use when:** You have a transaction id (from the list or a webhook) and need the full record before deciding to categorize, match, or attach a document.\n**Do not use for:** Walking the ledger: use the list endpoint with a cursor. Fetching the linked invoice/journal entry: separate endpoints.\n\n**Pitfalls:**\n- Both invoice_id (matched) and potential_invoice_id (suggested) can be set independently. The matched id is authoritative for accounting.\n- reconciliation_method is null for transactions that have never been auto-reconciled. journal_entry_id may still be set via manual categorize.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"transactions:read","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"date":{"type":"string"},"description":{"type":"string"},"amount":{"type":"number"},"currency":{"type":"string"},"amount_sek":{"type":"number"},"reference":{"type":"string"},"merchant_name":{"type":"string"},"counterparty_account":{"type":"string"},"journal_entry_id":{"type":"string"},"invoice_id":{"type":"string"},"supplier_invoice_id":{"type":"string"},"potential_invoice_id":{"type":"string"},"is_business":{"type":"boolean"},"category":{"type":"string"},"receipt_id":{"type":"string"},"document_id":{"type":"string"},"external_id":{"type":"string"},"import_source":{"type":"string"},"reconciliation_method":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","date","description","amount","currency","amount_sek","reference","merchant_name","counterparty_account","journal_entry_id","invoice_id","supplier_invoice_id","potential_invoice_id","is_business","category","receipt_id","document_id","external_id","import_source","reconciliation_method","created_at","updated_at"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/accounts":{"get":{"operationId":"accounts.list","summary":"List chart-of-accounts entries (BAS chart).","description":"Returns every account in the company's chart of accounts, ordered by sort_order (the BAS canonical sequence). Filter by ?class=<1..8> (BAS account class: 1=assets, 2=equity/liabilities, 3=revenue, 4=cost of goods sold, 5=övriga externa kostnader (rents, supplies, services), 6=övriga externa kostnader (marketing, professional services, IT), 7=labour, 8=financial). Note: BAS 5xxx and 6xxx are both övriga externa kostnader but cover distinct subgroups; see the BAS chart for the canonical mapping. Pass ?active=false to include archived accounts.\n\n**Use when:** You need account numbers and names to render verifikation tables, build a custom report, or look up the canonical BAS label for an account.\n**Do not use for:** Fetching balances: use the trial-balance report. Creating new accounts: this endpoint is read-only in v1 (use the dashboard).\n\n**Pitfalls:**\n- account_number is a STRING: \"1930\", not 1930. The leading character can be 0 in non-BAS plans.\n- is_system_account=true means the account was seeded by Accounted and cannot be archived or renamed.\n- Default filter excludes archived accounts; pass ?active=false to include them.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"reports:read","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"accounts":{"type":"array","items":{"type":"object","properties":{"account_number":{"type":"string"},"account_name":{"type":"string"},"account_class":{"type":"number"},"account_group":{"type":"string"},"account_type":{"type":"string"},"normal_balance":{"type":"string"},"is_system_account":{"type":"boolean"},"is_active":{"type":"boolean"},"description":{"type":"string"},"default_vat_code":{"type":"string"},"sru_code":{"type":"string"},"sort_order":{"type":"number"}},"required":["account_number","account_name","account_class","account_group","account_type","normal_balance","is_system_account","is_active","description","default_vat_code","sru_code","sort_order"],"additionalProperties":false}}},"required":["accounts"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/fiscal-periods":{"get":{"operationId":"fiscal-periods.list","summary":"List fiscal periods (räkenskapsår).","description":"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.\n\n**Use when:** You need to find the active period before booking, build a year-selector UI, or audit the period-lock history.\n**Do not 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.\n\n**Pitfalls:**\n- previous_period_id chains the bokslut continuity (BFNAR 2013:2). A null value on a non-first period is a data-quality red flag.\n- 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).\n- BFL 3 kap caps a single fiscal period at 18 months. First-year exceptions are allowed.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"reports:read","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"fiscal_periods":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"period_start":{"type":"string"},"period_end":{"type":"string"},"is_closed":{"type":"boolean"},"closed_at":{"type":"string"},"locked_at":{"type":"string"},"previous_period_id":{"type":"string"},"created_at":{"type":"string"},"duration_days":{"type":"number"},"exceeds_18_months":{"type":"boolean"}},"required":["id","name","period_start","period_end","is_closed","closed_at","locked_at","previous_period_id","created_at","duration_days","exceeds_18_months"],"additionalProperties":false}}},"required":["fiscal_periods"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/transactions/{id}/categorize":{"post":{"operationId":"transactions.categorize","summary":"Categorize a transaction and create the journal entry.","description":"Resolves the BAS account mapping for the transaction (via category, booking template, or counterparty template), creates the corresponding verifikation, and updates the transaction with is_business / category / journal_entry_id. Idempotent on (transaction, key). Dry-runnable.\n\n**Use when:** You're categorizing a bank transaction. Pass `is_business: true` plus either `category`, `template_id` (booking template), `counterparty_template_id`, or `account_override`. For private transactions, `is_business: false` is enough.\n**Do not use for:** Matching a payment to an invoice: use `:match-invoice` or `:match-supplier-invoice`, which storno any conflicting JE first. Uncategorizing: `:uncategorize`.\n\n**Pitfalls:**\n- A bank payment that looks like an invoice payment will be flagged via TX_CATEGORIZE_SUGGEST_SI_MATCH: pass `confirm_no_match: true` to override and force-categorize as direct expense (e.g. when the supplier invoice was already booked).\n- Already-categorized fast path: if the transaction already has a journal_entry_id, only flags get updated. The JE is immutable post-commit.\n- account_override must exist in the chart of accounts; an unknown account returns TX_CATEGORIZE_INVALID_ACCOUNT.","x-action-risk":"medium","x-idempotent":true,"x-reversible":true,"x-dry-run-supported":true,"x-required-scope":"transactions:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"success":{"type":"boolean"},"journal_entry_created":{"type":"boolean"},"journal_entry_id":{"type":"string"},"journal_entry_error":{"type":"string"},"document_link_warning":{"type":"string"},"category":{"type":"string"},"already_had_journal_entry":{"type":"boolean"}},"required":["success","journal_entry_created","journal_entry_id","journal_entry_error","category"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/transactions/{id}/uncategorize":{"post":{"operationId":"transactions.uncategorize","summary":"Reverse the categorization of a transaction (storno + reset).","description":"Storno the transaction's journal entry (BFL 5 kap 5 §: posted entries are never deleted, only cancelled via a reversing entry) and reset is_business / category / journal_entry_id on the transaction row. Idempotent: a second call on an already-uncategorized transaction returns 400 TX_UNCATEGORIZE_NOT_BOOKED. Dry-runnable.\n\n**Use when:** You categorized a transaction by mistake and want to redo it from scratch. The storno keeps the audit trail intact.\n**Do not use for:** Changing the categorization of an already-booked transaction: categorize again instead (the second call sees journal_entry_id and only updates flags). Reversing a payment match: there is no v1 verb for that yet.\n\n**Pitfalls:**\n- Idempotency-Key is mandatory.\n- The storno creates a new (cancelling) journal entry. The original entry stays in the ledger marked as cancelled: voucher gaps are documented automatically.\n- A transaction without a journal_entry_id returns 400 TX_UNCATEGORIZE_NOT_BOOKED: there is nothing to reverse.","x-action-risk":"medium","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":true,"x-required-scope":"transactions:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"success":{"type":"boolean"},"reversed_journal_entry_id":{"type":"string"}},"required":["success","reversed_journal_entry_id"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/transactions/{id}/match-invoice":{"post":{"operationId":"transactions.match-invoice","summary":"Match a positive bank transaction to a customer invoice.","description":"Confirms an invoice match for a transaction. Storno any conflicting auto-categorization JE, create the payment journal entry, update the invoice status (paid / partially_paid), insert into invoice_payments, and link the transaction. Idempotent.\n\n**Use when:** You have a bank receipt and a known open invoice it pays. The transaction must be positive (income) and unlinked.\n**Do not use for:** Categorizing a transaction without an invoice: use `:categorize`. Matching to a supplier invoice: use `:match-supplier-invoice`. Bulk auto-match: use `POST /reconciliation/bank/run`.\n\n**Pitfalls:**\n- Proforma + delivery notes are rejected (MATCH_INVOICE_NOT_INVOICE_TYPE): only document_type='invoice' can be matched.\n- Transaction must be positive (amount > 0): negative transactions return MATCH_INVOICE_NOT_INCOME.\n- Invoice must be in sent / overdue / partially_paid status: paid or draft invoices return MATCH_INVOICE_NOT_OPEN.\n- Idempotency-Key is mandatory.","x-action-risk":"high","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"transactions:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"success":{"type":"boolean"},"invoice_status":{"type":"string"},"paid_at":{"type":"string"},"paid_amount":{"type":"number"},"remaining_amount":{"type":"number"},"journal_entry_id":{"type":"string"},"category":{"type":"string"}},"required":["success","invoice_status","paid_at","paid_amount","remaining_amount","journal_entry_id","category"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/transactions/{id}/match-supplier-invoice":{"post":{"operationId":"transactions.match-supplier-invoice","summary":"Match a negative bank transaction to a supplier invoice.","description":"Confirms a supplier invoice payment match. Creates the payment journal entry (accrual: 2440 debit / 1930 credit; cash-method: collapsed registration+payment), updates supplier_invoices, inserts a supplier_invoice_payments row, and links the transaction. Handles FX differences for cross-currency payments (7960 gain / 3960 loss).\n\n**Use when:** You have a bank payment and a known open supplier invoice. The transaction must be negative (expense) and unlinked.\n**Do not use for:** Categorizing a direct supplier expense without an invoice: use `:categorize`. Matching to a customer invoice: use `:match-invoice`. Bulk auto-match: `POST /reconciliation/bank/run`.\n\n**Pitfalls:**\n- Cash-method companies can settle a foreign invoice in full (booked at the payment-date rate); only a PARTIAL cash-method payment across currencies is rejected (MATCH_SI_CASH_FX_UNSUPPORTED): pay in full, switch to accrual, or book manually.\n- Transaction must be negative (amount < 0). Positive returns MATCH_SI_NOT_EXPENSE.\n- Supplier invoice must NOT be paid/credited already. paid/credited returns MATCH_SI_ALREADY_PAID; registered/approved/partially_paid/overdue are matchable.\n- Idempotency-Key is mandatory.","x-action-risk":"high","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"transactions:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"success":{"type":"boolean"},"invoice_status":{"type":"string"},"paid_amount":{"type":"number"},"remaining_amount":{"type":"number"},"journal_entry_id":{"type":"string"}},"required":["success","invoice_status","paid_amount","remaining_amount","journal_entry_id"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/transactions/ingest":{"post":{"operationId":"transactions.ingest","summary":"Bulk-ingest transactions (up to 500 per call).","description":"Runs the same ingest pipeline as the dashboard CSV importer and the PSD2 bank sync: dedup, insert, invoice match, mapping-rule auto-categorize, auto-JE for high-confidence matches. Idempotent over the whole batch via Idempotency-Key. Dry-runnable.\n\n**Use when:** You're importing transactions from a CSV, a custom bank feed, or an external accounting system. Each item must have a stable external_id: this is the primary dedup key.\n**Do not use for:** Single ad-hoc transactions (use the dashboard). Documents/receipts (use the documents endpoint). Manually-created journal entries (Phase 4).\n\n**Pitfalls:**\n- external_id is the primary dedup key: make it stable for the same physical transaction across reruns.\n- Content-based dedup runs in addition: a row matching an already-booked transaction by date, amount AND description (prefix-containment, to survive PSD2 title enrichment) is skipped even if external_id differs.\n- raw_insert_only=true skips ALL post-insert pipeline steps (matching, categorization). Use for viewer-only imports.\n- Max 500 items per call. For larger imports, split into pages of 500.\n- Dry-run previews external_id + content dedup against BOOKED rows only; the live pipeline also dedups against unbooked bank-synced rows, so preview skips are a lower bound on the live skip count.","x-action-risk":"medium","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":true,"x-required-scope":"transactions:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"imported":{"type":"number"},"duplicates":{"type":"number"},"reconciled":{"type":"number"},"auto_categorized":{"type":"number"},"auto_matched_invoices":{"type":"number"},"errors":{"type":"number"},"transaction_ids":{"type":"array","items":{"type":"string"}}},"required":["imported","duplicates","reconciled","auto_categorized","auto_matched_invoices","errors","transaction_ids"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/transactions/batch-categorize":{"post":{"operationId":"transactions.batch-categorize","summary":"Categorize up to 100 transactions in one call (partial-success).","description":"Per-item categorization mirroring the single :categorize endpoint. Same `{ results, summary }` shape as the other bulk endpoints. all_or_nothing: true returns 501 NOT_IMPLEMENTED. Idempotent over the whole batch.\n\n**Use when:** You have many transactions to categorize with the same logic (e.g. apply a booking template across a queue, mark a batch as private, override accounts on a series).\n**Do not use for:** Categorizing transactions with mixed logic: make multiple :categorize calls. Auto-categorization via templates: handled inside `ingest` for matching rows, no separate endpoint needed.\n\n**Pitfalls:**\n- Max 100 items per call. Sequential processing.\n- Idempotency-Key covers the WHOLE batch: replays return the cached full response.\n- all_or_nothing: true returns 501 NOT_IMPLEMENTED. Today only partial-success batches exist.","x-action-risk":"medium","x-idempotent":true,"x-reversible":true,"x-dry-run-supported":true,"x-required-scope":"transactions:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object","properties":{"ok":{"type":"boolean"},"request_index":{"type":"number"},"transaction_id":{"type":"string"},"data":{},"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["ok","request_index","transaction_id"],"additionalProperties":false}},"summary":{"type":"object","properties":{"total":{"type":"number"},"succeeded":{"type":"number"},"failed":{"type":"number"}},"required":["total","succeeded","failed"],"additionalProperties":false}},"required":["results","summary"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/reconciliation/bank/run":{"post":{"operationId":"reconciliation.bank.run","summary":"Run the bank-reconciliation matcher.","description":"Walks all unbooked bank transactions in the requested date range and pairs them with open GL lines (1930-side) by amount + date proximity. Applies confirmed matches by setting transactions.journal_entry_id (the GL row already exists). Dry-runnable.\n\n**Use when:** You want to auto-match outstanding bank transactions against existing journal entries: typically as the closing step of a sync. Dry-run first to inspect proposed matches.\n**Do not use for:** Creating new journal entries: this only links bank transactions to existing GL lines. Matching to invoices: use `:match-invoice` or `:match-supplier-invoice` for explicit invoice payments.\n\n**Pitfalls:**\n- date_from / date_to default to the company's full bank history if omitted. Specify a window for predictable performance.\n- account_number defaults to 1930. Multi-account companies must pass the BAS code of the account they are reconciling (e.g. 1932 for a EUR account), or it silently reconciles 1930.\n- Idempotency-Key is mandatory.\n- Without confidence_threshold, a non-dry run applies EVERY match found, including fuzzy ones at confidence 0.75. Pass confidence_threshold (0.9 recommended, matching gnubok_auto_match_period) for unattended runs, or dry-run first and review matches.confidence before applying. Matches below the threshold are returned but not applied (skipped_below_threshold counts them).\n- The 366-day window bound only applies when BOTH date_from and date_to are set; a single-sided or absent window scans full history.","x-action-risk":"medium","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":true,"x-required-scope":"transactions:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"matches":{"type":"array","items":{"type":"object","properties":{"transaction_id":{"type":"string"},"transaction_date":{"type":"string"},"transaction_description":{"type":"string"},"transaction_amount":{"type":"number"},"journal_entry_id":{"type":"string"},"voucher_number":{"type":"number"},"voucher_series":{"type":"string"},"entry_date":{"type":"string"},"entry_description":{"type":"string"},"method":{"type":"string"},"confidence":{"type":"number"}},"required":["transaction_id","transaction_date","transaction_description","transaction_amount","journal_entry_id","voucher_number","voucher_series","entry_date","entry_description","method","confidence"],"additionalProperties":false}},"applied":{"type":"number"},"errors":{"type":"number"},"skipped_below_threshold":{"type":"number"}},"required":["matches","applied","errors","skipped_below_threshold"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/reconciliation/bank/status":{"get":{"operationId":"reconciliation.bank.status","summary":"Bank-reconciliation health snapshot.","description":"Returns matched / unmatched counts and the balance delta between the bank ledger and the GL for the requested window. Optional ?date_from / ?date_to (default: company history).\n\n**Use when:** You're building a dashboard widget, an audit report, or a pre-close check that needs to know how many bank transactions are still unbooked.\n**Do not use for:** Running the matcher: that's POST `/reconciliation/bank/run`. Per-transaction detail: use the transaction list with `?status=unbooked`.\n\n**Pitfalls:**\n- A non-zero difference is normal between sync runs (uncleared cheques, in-flight transfers). Investigate only if it persists across reconciliations.\n- difference compares against gl_1930_period_movement (movement excl. opening balance), NOT gl_1930_balance. Do not display gl_1930_balance next to difference.\n- is_reconciled means |difference| < 0.01 for the window, an aggregate check, not a per-transaction guarantee.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"transactions:read","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"bank_transaction_total":{"type":"number"},"gl_1930_balance":{"type":"number"},"gl_1930_period_movement":{"type":"number"},"gl_1930_opening_balance":{"type":"number"},"gl_1930_correction_adjustment":{"type":"number"},"difference":{"type":"number"},"is_reconciled":{"type":"boolean"},"matched_count":{"type":"number"},"unmatched_transaction_count":{"type":"number"},"unmatched_gl_line_count":{"type":"number"}},"required":["bank_transaction_total","gl_1930_balance","gl_1930_period_movement","gl_1930_opening_balance","gl_1930_correction_adjustment","difference","is_reconciled","matched_count","unmatched_transaction_count","unmatched_gl_line_count"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/suppliers":{"get":{"operationId":"suppliers.list","summary":"List suppliers for a company.","description":"Returns active suppliers in created-first order. Pass ?include_archived=true to include archived rows. Use ?search to match against name or org_number.\n\n**Use when:** You need a supplier roster: for building a UI picker, resolving a supplier_id before registering a supplier invoice, or syncing an external AP system.\n**Do not use for:** Fetching a single supplier you already know the id of: use GET /api/v1/companies/{companyId}/suppliers/{id}. Customers are a separate resource.\n\n**Pitfalls:**\n- Archived suppliers are hidden by default; the dashboard makes the same choice.\n- org_number identifies legal entities only: suppliers currently have no `individual` type, so the field is Bolagsverket public-record data when present.\n- vat_number is stored as supplied; unlike customers, suppliers are not auto-validated against VIES on create. Validate externally if the integration requires it.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"suppliers:read","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"supplier_type":{"type":"string","enum":["swedish_business","eu_business","non_eu_business"]},"email":{"type":"string"},"org_number":{"type":"string"},"vat_number":{"type":"string"},"default_payment_terms":{"type":"number"},"default_currency":{"type":"string"},"archived_at":{"type":"string"},"created_at":{"type":"string"}},"required":["id","name","supplier_type","email","org_number","vat_number","default_payment_terms","default_currency","archived_at","created_at"],"additionalProperties":false}},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}},"post":{"operationId":"suppliers.create","summary":"Create a supplier.","description":"Creates a new supplier for the company. Requires Idempotency-Key (UUID). Supports ?dry_run=true for input validation without committing: the dry-run response shows the would-be record minus id and timestamps.\n\n**Use when:** You need to register a new supplier before booking supplier invoices against them. Use dry-run first to catch validation errors before committing.\n**Do not use for:** Updating an existing supplier (PATCH instead). Creating customers (different resource).\n\n**Pitfalls:**\n- Idempotency-Key is mandatory: calls without it return 400 VALIDATION_ERROR.\n- org_number uniqueness is enforced at the database level; duplicate inserts return 409 SUPPLIER_DUPLICATE_ORG_NUMBER.\n- Unlike customers, suppliers carry no `vat_number_validated` flag: vat_number is stored as supplied without VIES verification. Validate externally if your workflow requires it.\n- default_expense_account is a BAS account number (e.g. \"5410\"); the value is stored as-is and used as the suggested debit account when supplier invoices are booked.","x-action-risk":"low","x-idempotent":true,"x-reversible":true,"x-dry-run-supported":true,"x-required-scope":"suppliers:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"supplier_type":{"type":"string","enum":["swedish_business","eu_business","non_eu_business"]},"email":{"type":"string"},"phone":{"type":"string"},"address_line1":{"type":"string"},"address_line2":{"type":"string"},"postal_code":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"org_number":{"type":"string"},"vat_number":{"type":"string"},"bankgiro":{"type":"string"},"plusgiro":{"type":"string"},"bank_account":{"type":"string"},"iban":{"type":"string"},"bic":{"type":"string"},"default_expense_account":{"type":"string"},"default_payment_terms":{"type":"number"},"default_currency":{"type":"string"},"notes":{"type":"string"},"archived_at":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","name","supplier_type","email","phone","address_line1","address_line2","postal_code","city","country","org_number","vat_number","bankgiro","plusgiro","bank_account","iban","bic","default_expense_account","default_payment_terms","default_currency","notes","archived_at","created_at","updated_at"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/suppliers/{id}":{"get":{"operationId":"suppliers.get","summary":"Retrieve a single supplier by id.","description":"Returns the full supplier record. Pass ?expand=supplier_invoices to embed any open supplier invoices (registered / approved / partially_paid / overdue / disputed) for the supplier in the same response.\n\n**Use when:** You need the full supplier record: address, payment terms, banking details, default expense account: before booking a supplier invoice or syncing to an external AP system.\n**Do not use for:** Listing suppliers (use the list endpoint). Looking up customer or employee records (different resources).\n\n**Pitfalls:**\n- archived_at is non-null when the supplier has been soft-deleted; the supplier is still queryable by id but excluded from default lists.\n- Banking fields (bankgiro / plusgiro / iban / bic) are stored as supplied; no Luhn or IBAN check is performed at this layer.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"suppliers:read","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"supplier_type":{"type":"string"},"email":{"type":"string"},"phone":{"type":"string"},"address_line1":{"type":"string"},"address_line2":{"type":"string"},"postal_code":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"org_number":{"type":"string"},"vat_number":{"type":"string"},"bankgiro":{"type":"string"},"plusgiro":{"type":"string"},"bank_account":{"type":"string"},"iban":{"type":"string"},"bic":{"type":"string"},"default_expense_account":{"type":"string"},"default_payment_terms":{"type":"number"},"default_currency":{"type":"string"},"notes":{"type":"string"},"archived_at":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","name","supplier_type","email","phone","address_line1","address_line2","postal_code","city","country","org_number","vat_number","bankgiro","plusgiro","bank_account","iban","bic","default_expense_account","default_payment_terms","default_currency","notes","archived_at","created_at","updated_at"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}},"patch":{"operationId":"suppliers.update","summary":"Partially update a supplier.","description":"Patches the supplier with the supplied fields. All fields optional. Idempotent (mandatory Idempotency-Key). Dry-runnable.\n\n**Use when:** You need to change a supplier's contact details, payment terms, banking info, default expense account, or VAT number. Use dry-run first to confirm the merged record before committing.\n**Do not use for:** Archiving a supplier (use DELETE: sets archived_at). Replacing the entire record (no PUT verb is exposed; PATCH is partial).\n\n**Pitfalls:**\n- Idempotency-Key is mandatory; calls without it return 400.\n- org_number uniqueness is enforced at DB level: 23505 → 409 SUPPLIER_DUPLICATE_ORG_NUMBER.\n- Changing default_expense_account does not retroactively rebook prior supplier invoices: only future bookings pick up the new default.","x-action-risk":"low","x-idempotent":true,"x-reversible":true,"x-dry-run-supported":true,"x-required-scope":"suppliers:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"supplier_type":{"type":"string"},"email":{"type":"string"},"phone":{"type":"string"},"address_line1":{"type":"string"},"address_line2":{"type":"string"},"postal_code":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"org_number":{"type":"string"},"vat_number":{"type":"string"},"bankgiro":{"type":"string"},"plusgiro":{"type":"string"},"bank_account":{"type":"string"},"iban":{"type":"string"},"bic":{"type":"string"},"default_expense_account":{"type":"string"},"default_payment_terms":{"type":"number"},"default_currency":{"type":"string"},"notes":{"type":"string"},"archived_at":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","name","supplier_type","email","phone","address_line1","address_line2","postal_code","city","country","org_number","vat_number","bankgiro","plusgiro","bank_account","iban","bic","default_expense_account","default_payment_terms","default_currency","notes","archived_at","created_at","updated_at"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}},"delete":{"operationId":"suppliers.delete","summary":"Archive a supplier (soft-delete).","description":"Sets archived_at on the supplier; the record is preserved (supplier invoices and audit history remain intact) but excluded from default list responses. To un-archive, PATCH archived_at back to null. Idempotent: archiving an already-archived supplier is a no-op. Dry-runnable.\n\n**Use when:** You want to remove a supplier from active rosters without losing their history. Idempotent: re-archiving is safe.\n**Do not use for:** Permanently deleting a supplier with all history: the public API does not expose hard-delete. GDPR erasure requests go through a dedicated workflow.\n\n**Pitfalls:**\n- Idempotency-Key is mandatory.\n- A supplier with any open supplier invoice (registered / approved / partially_paid / overdue / disputed) cannot be archived: returns 409 SUPPLIER_HAS_INVOICES. Close the invoices first. This protects BFL 7 kap audit: the supplier record is the canonical source of seller name/address for invoice reissuance.\n- 204 No Content is returned on success: there is no response body to parse.","x-action-risk":"medium","x-idempotent":true,"x-reversible":true,"x-dry-run-supported":true,"x-required-scope":"suppliers:write","responses":{"204":{"description":"No Content"},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/suppliers/bulk-create":{"post":{"operationId":"suppliers.bulk-create","summary":"Create up to 50 suppliers in one call (partial-success).","description":"Bulk-create endpoint mirroring /customers/bulk-create. Each supplier is validated and inserted independently: per-item failures do not roll back items that succeeded. Returns a results array plus a summary. Idempotent over the whole batch. Dry-runnable.\n\n**Use when:** You're importing a roster of suppliers from another AP system, or seeding a fresh company with its existing vendor list. Use dry-run first to validate the batch.\n**Do not use for:** Updating existing suppliers: PATCH /suppliers/{id} once per supplier. Bulk uploads of > 50 suppliers: split into pages of 50. Transactional all-or-nothing imports: passing all_or_nothing: true returns 501 NOT_IMPLEMENTED.\n\n**Pitfalls:**\n- Idempotency-Key is mandatory and covers the WHOLE batch. A retried bulk-create returns the cached full response: it does not retry only the failed items.\n- Passing all_or_nothing: true returns 501 NOT_IMPLEMENTED. Today only partial-success batches exist; omit the flag or pass false.\n- org_number uniqueness is enforced at the DB level: items with duplicates fail individually with SUPPLIER_DUPLICATE_ORG_NUMBER.\n- No VIES validation runs per item; vat_number is stored as supplied. Validate externally if your workflow requires it.","x-action-risk":"low","x-idempotent":true,"x-reversible":true,"x-dry-run-supported":true,"x-required-scope":"suppliers:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object","properties":{"ok":{"type":"boolean"},"request_index":{"type":"number"},"data":{},"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["ok","request_index"],"additionalProperties":false}},"summary":{"type":"object","properties":{"total":{"type":"number"},"succeeded":{"type":"number"},"failed":{"type":"number"}},"required":["total","succeeded","failed"],"additionalProperties":false}},"required":["results","summary"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/supplier-invoices":{"get":{"operationId":"supplier-invoices.list","summary":"List supplier invoices for a company.","description":"Returns supplier invoices in most-recent-first order. Filters: status, supplier_id, currency, date_from / date_to (filter by invoice_date).\n\n**Use when:** You need to enumerate registered supplier invoices for an AP dashboard, a payment run, or a leverantörsreskontra reconciliation.\n**Do not use for:** Fetching a single supplier invoice: use GET /supplier-invoices/{id}. Listing customer invoices (different resource).\n\n**Pitfalls:**\n- Credit notes (is_credit_note=true) appear in the same list as the originals; filter by status=credited or check the flag to separate.\n- remaining_amount is the unpaid portion; a partially_paid SI has remaining_amount > 0.\n- arrival_number is internal book-keeping, not the seller's invoice number: use supplier_invoice_number for matching to received documents.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"suppliers:read","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"supplier_id":{"type":"string"},"supplier_name":{"type":"string"},"arrival_number":{"type":"number"},"supplier_invoice_number":{"type":"string"},"invoice_date":{"type":"string"},"due_date":{"type":"string"},"status":{"type":"string","enum":["registered","approved","paid","partially_paid","overdue","disputed","credited","reversed"]},"currency":{"type":"string"},"subtotal":{"type":"number"},"vat_amount":{"type":"number"},"total":{"type":"number"},"paid_amount":{"type":"number"},"remaining_amount":{"type":"number"},"is_credit_note":{"type":"boolean"},"paid_at":{"type":"string"},"created_at":{"type":"string"}},"required":["id","supplier_id","supplier_name","arrival_number","supplier_invoice_number","invoice_date","due_date","status","currency","subtotal","vat_amount","total","paid_amount","remaining_amount","is_credit_note","paid_at","created_at"],"additionalProperties":false}},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}},"post":{"operationId":"supplier-invoices.create","summary":"Register a new supplier invoice.","description":"Creates a supplier invoice in `registered` status and posts the registration journal entry under faktureringsmetoden (Debit expense + Debit 2641 Ingående moms / Credit 2440 Leverantörsskulder). Under kontantmetoden no JE is posted at this stage. Idempotent (mandatory Idempotency-Key). Dry-runnable.\n\n**Use when:** You're registering an incoming leverantörsfaktura. Use dry-run first to validate VAT calculations + period-lock state before committing.\n**Do not use for:** Marking an existing SI as paid (use POST /:id/mark-paid). Issuing a credit note (use POST /:id/credit). Customer invoices (different resource).\n\n**Pitfalls:**\n- Idempotency-Key is mandatory.\n- invoice_date must fall within an open fiscal period: a date covered by a locked period or the company-wide bookkeeping lock returns 400 PERIOD_LOCKED.\n- Under faktureringsmetoden the registration JE is posted atomically with the SI row. JE failure aborts the whole call and no SI row is left behind (strict-mode).\n- supplier_id must reference an existing, non-archived supplier in the same company: 404 SUPPLIER_NOT_FOUND otherwise.\n- Duplicate (supplier_id, supplier_invoice_number) returns 409 SI_CREATE_DUPLICATE_INVOICE_NUMBER. Use the credit flow on the original instead of re-registering with a tweaked number.\n- Project/cost-center tagging: pass default_dimensions ({\"6\":\"P001\"} = project, {\"1\":\"KS01\"} = kostnadsställe) for the whole invoice and/or items[].dimensions per line (per-line wins per key). The registration JE lines are tagged accordingly. When the company has the dimension registry enabled, unknown or archived codes are rejected with 400 DIMENSION_VALIDATION_FAILED — list valid codes via GET /dimensions.","x-action-risk":"medium","x-idempotent":true,"x-reversible":true,"x-dry-run-supported":true,"x-required-scope":"suppliers:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"supplier_id":{"type":"string"},"arrival_number":{"type":"number"},"supplier_invoice_number":{"type":"string"},"invoice_date":{"type":"string"},"due_date":{"type":"string"},"status":{"type":"string"},"currency":{"type":"string"},"subtotal":{"type":"number"},"vat_amount":{"type":"number"},"total":{"type":"number"},"remaining_amount":{"type":"number"},"is_credit_note":{"type":"boolean"},"registration_journal_entry_id":{"type":"string"},"created_at":{"type":"string"}},"required":["id","supplier_id","arrival_number","supplier_invoice_number","invoice_date","due_date","status","currency","subtotal","vat_amount","total","remaining_amount","is_credit_note","registration_journal_entry_id","created_at"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/supplier-invoices/{id}":{"get":{"operationId":"supplier-invoices.get","summary":"Retrieve a single supplier invoice by id.","description":"Returns the full supplier-invoice record. Pass ?expand=supplier,items,payments to embed the related rows in the same response.\n\n**Use when:** You need the full record before approving, paying, or crediting it, or for audit trail / reconciliation.\n**Do not use for:** Listing supplier invoices (use the list endpoint). Customer-invoice lookups (different resource).\n\n**Pitfalls:**\n- Credit notes return is_credit_note=true and a credited_invoice_id pointing at the original.\n- registration_journal_entry_id and payment_journal_entry_id let you trace the SI to its bokföring rows; they are null when no JE has been posted (e.g. on a kontantmetoden SI before payment).","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"suppliers:read","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"supplier_id":{"type":"string"},"arrival_number":{"type":"number"},"supplier_invoice_number":{"type":"string"},"invoice_date":{"type":"string"},"due_date":{"type":"string"},"received_date":{"type":"string"},"delivery_date":{"type":"string"},"status":{"type":"string"},"currency":{"type":"string"},"exchange_rate":{"type":"number"},"subtotal":{"type":"number"},"vat_amount":{"type":"number"},"total":{"type":"number"},"vat_treatment":{"type":"string"},"reverse_charge":{"type":"boolean"},"paid_amount":{"type":"number"},"remaining_amount":{"type":"number"},"is_credit_note":{"type":"boolean"},"credited_invoice_id":{"type":"string"},"registration_journal_entry_id":{"type":"string"},"payment_journal_entry_id":{"type":"string"},"notes":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","supplier_id","arrival_number","supplier_invoice_number","invoice_date","due_date","received_date","delivery_date","status","currency","exchange_rate","subtotal","vat_amount","total","vat_treatment","reverse_charge","paid_amount","remaining_amount","is_credit_note","credited_invoice_id","registration_journal_entry_id","payment_journal_entry_id","notes","created_at","updated_at"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}},"patch":{"operationId":"supplier-invoices.update","summary":"Update a registered supplier invoice.","description":"Patches a supplier invoice with the supplied fields. Only allowed on `registered` status: once approved, paid, or credited, the record is effectively immutable from the API's perspective. Idempotent (mandatory Idempotency-Key). Dry-runnable.\n\n**Use when:** You need to fix a typo in supplier_invoice_number, adjust dates, or attach a payment reference / notes to a registered SI before approval. Use dry-run to confirm the merged state first.\n**Do not use for:** Editing line items (immutable: credit the SI and register a new one). Changing status (use action verbs). Approved/paid/credited SIs (returns 400 SI_NOT_DRAFT).\n\n**Pitfalls:**\n- Returns 400 SI_NOT_DRAFT when current status !== \"registered\".\n- invoice_date / due_date changes do not re-post the registration JE; if the entry date needs to change, credit the SI and re-register.","x-action-risk":"low","x-idempotent":true,"x-reversible":true,"x-dry-run-supported":true,"x-required-scope":"suppliers:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"supplier_id":{"type":"string"},"arrival_number":{"type":"number"},"supplier_invoice_number":{"type":"string"},"invoice_date":{"type":"string"},"due_date":{"type":"string"},"received_date":{"type":"string"},"delivery_date":{"type":"string"},"status":{"type":"string"},"currency":{"type":"string"},"exchange_rate":{"type":"number"},"subtotal":{"type":"number"},"vat_amount":{"type":"number"},"total":{"type":"number"},"vat_treatment":{"type":"string"},"reverse_charge":{"type":"boolean"},"paid_amount":{"type":"number"},"remaining_amount":{"type":"number"},"is_credit_note":{"type":"boolean"},"credited_invoice_id":{"type":"string"},"registration_journal_entry_id":{"type":"string"},"payment_journal_entry_id":{"type":"string"},"notes":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","supplier_id","arrival_number","supplier_invoice_number","invoice_date","due_date","received_date","delivery_date","status","currency","exchange_rate","subtotal","vat_amount","total","vat_treatment","reverse_charge","paid_amount","remaining_amount","is_credit_note","credited_invoice_id","registration_journal_entry_id","payment_journal_entry_id","notes","created_at","updated_at"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/supplier-invoices/{id}/approve":{"post":{"operationId":"supplier-invoices.approve","summary":"Approve a registered supplier invoice.","description":"Flips a supplier invoice from `registered` to `approved`. No journal entry is posted here: the registration JE was already booked at :create under accrual, or is deferred to :mark-paid under cash. Idempotent. Dry-runnable.\n\n**Use when:** A registered SI has been reviewed and you want to mark it ready for payment. Many AP workflows gate :mark-paid behind an explicit approval step.\n**Do not use for:** Posting a journal entry (already done at :create under accrual). Paying the SI (use :mark-paid). Re-approving an already-approved SI (returns 400 SI_APPROVE_NOT_REGISTERED).\n\n**Pitfalls:**\n- Idempotency-Key is mandatory.\n- Returns 400 SI_APPROVE_NOT_REGISTERED when current status !== \"registered\". Use the detail endpoint to inspect status first if unsure.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":true,"x-required-scope":"suppliers:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"status":{"const":"approved"},"arrival_number":{"type":"number"},"supplier_invoice_number":{"type":"string"}},"required":["id","status","arrival_number","supplier_invoice_number"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/supplier-invoices/{id}/mark-paid":{"post":{"operationId":"supplier-invoices.mark-paid","summary":"Record a payment against a supplier invoice.","description":"Books the payment journal entry (Debit 2440 / Credit 1930 under accrual; or Debit expense + Debit 2641 / Credit 1930 under cash) and flips the SI status to `paid` (full settlement) or `partially_paid`. Strict-mode: a JE failure aborts before any SI mutation. Idempotent. Dry-runnable.\n\n**Use when:** You paid a registered or approved leverantörsfaktura through a channel other than the synced bank flow. For bank-matched payments use POST /transactions/{id}/match-supplier-invoice instead: that path also reconciles the bank line.\n**Do not use for:** Refunding a payment (the public API does not expose unmark-paid; credit the SI instead). Paying a credited or already-paid SI (returns 409 SI_PAID_ALREADY).\n\n**Pitfalls:**\n- Idempotency-Key is mandatory.\n- payment_date must fall in an open fiscal period: locked period returns 400 PERIOD_LOCKED.\n- exchange_rate_difference (SEK delta vs the booked rate at registration) is required for foreign-currency SIs to book the FX gain/loss to 3960 / 7960. Omitting it on a non-SEK SI under accrual mis-books FX.\n- Strict-mode: a JE creation failure ABORTS before the status flip. There is no partial-state recovery banner: retry the call.\n- Cash basis (kontantmetoden) recognizes the expense + ingående moms HERE, not at :create.","x-action-risk":"medium","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":true,"x-required-scope":"suppliers:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["paid","partially_paid"]},"total":{"type":"number"},"paid_amount":{"type":"number"},"remaining_amount":{"type":"number"},"paid_at":{"type":"string"},"payment_journal_entry_id":{"type":"string"}},"required":["id","status","total","paid_amount","remaining_amount","paid_at","payment_journal_entry_id"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/supplier-invoices/{id}/credit":{"post":{"operationId":"supplier-invoices.credit","summary":"Issue a credit note for a supplier invoice.","description":"Creates a kreditfaktura that reverses the original supplier invoice. Under accrual the reversing JE is posted atomically (Debit 2440 / Credit expense + Credit 2641). The original status flips to `credited`. Strict-mode: any failure rolls back the credit-note row. Idempotent. Dry-runnable.\n\n**Use when:** You need to nullify a registered, approved, partially_paid, or paid supplier invoice: for a returned shipment, an over-invoice, or a vendor dispute resolution. Use dry-run to confirm the totals first.\n**Do not use for:** Editing line items on an unchanged invoice (use PATCH on `registered` SIs). Crediting an already-credited SI (returns 409 SI_CREDIT_ALREADY_CREDITED). Reversing a v1-issued credit (no v1 endpoint today: use the dashboard).\n\n**Pitfalls:**\n- Idempotency-Key is mandatory.\n- Today's date is used as the credit-note invoice_date. It must fall in an open fiscal period: locked period returns 400 SI_CREDIT_PERIOD_LOCKED.\n- Cash basis (kontantmetoden): no reversing JE is posted: recognition is deferred until a refund transaction is booked. The credit-note row is still created so the AP audit trail stays consistent.\n- The original SI is flipped to `credited` regardless of how much of it was already paid; reconcile the bank refund via the transactions endpoints.","x-action-risk":"high","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":true,"x-required-scope":"suppliers:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"credit_note_id":{"type":"string"},"original_id":{"type":"string"},"arrival_number":{"type":"number"},"supplier_invoice_number":{"type":"string"},"registration_journal_entry_id":{"type":"string"}},"required":["credit_note_id","original_id","arrival_number","supplier_invoice_number","registration_journal_entry_id"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/employees":{"get":{"operationId":"employees.list","summary":"List employees for a company.","description":"Returns active employees in created-first order. Pass ?include_inactive=true to include soft-deleted (is_active=false) rows. Use ?search to match against first or last name. Personnummer is masked (birthdate visible, last-4 hidden); use GET /employees/{id} for the full value.\n\n**Use when:** You need a roster: for building a UI picker, resolving employee_id before adding to a salary run, or syncing an external HR system.\n**Do not use for:** Fetching a single employee you already know the id of: use GET /api/v1/companies/{companyId}/employees/{id}. Salary calculations live on /salary-runs/{id}.\n\n**Pitfalls:**\n- Inactive employees are hidden by default; soft-delete via DELETE sets is_active=false (BFL 7 kap retention).\n- personnummer is masked in the list response (GDPR Art.5(1)(c) data minimisation). The detail endpoint returns the full value.\n- salary_type drives which field is meaningful: monthly_salary for monthly, hourly_rate for hourly. The other is null.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"payroll:read","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"first_name":{"type":"string"},"last_name":{"type":"string"},"personnummer_masked":{"type":"string"},"employment_type":{"type":"string","enum":["employee","company_owner","board_member"]},"employment_start":{"type":"string"},"employment_end":{"type":"string"},"salary_type":{"type":"string","enum":["monthly","hourly"]},"monthly_salary":{"type":"number"},"hourly_rate":{"type":"number"},"f_skatt_status":{"type":"string","enum":["a_skatt","f_skatt","fa_skatt","not_verified"]},"is_active":{"type":"boolean"},"created_at":{"type":"string"}},"required":["id","first_name","last_name","personnummer_masked","employment_type","employment_start","employment_end","salary_type","monthly_salary","hourly_rate","f_skatt_status","is_active","created_at"],"additionalProperties":false}},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}},"post":{"operationId":"employees.create","summary":"Create an employee.","description":"Creates a new employee for the company. Requires Idempotency-Key (UUID). Supports ?dry_run=true for input validation without committing. The personnummer in the request body must be 12 digits (ÅÅÅÅMMDDNNNN); the response echoes a masked form (birthdate + XXXX): GDPR Art.5(1)(c).\n\n**Use when:** You need to register a new employee before adding them to a salary run. Use dry-run first to catch validation errors (missing tax table, salary amount, F-skatt mismatch) before committing.\n**Do not use for:** Updating an existing employee (PATCH instead). Soft-deactivating (DELETE: sets is_active=false). Hard-deleting (the API does not expose hard delete; BFL 7 kap retention).\n\n**Pitfalls:**\n- Idempotency-Key is mandatory: calls without it return 400 VALIDATION_ERROR.\n- personnummer must be exactly 12 digits with the YYYYMMDD prefix (not the short 10-digit form).\n- Duplicate personnummer within a company returns 409 EMPLOYEE_DUPLICATE_PERSONNUMMER. Personnummer is unique per (company_id, personnummer).\n- For A-skatt employees who are not sidoinkomst, tax_table_number is required (29-42).\n- salary_type drives which salary field is required: monthly_salary for monthly, hourly_rate for hourly.\n- The response masks personnummer; never echo back the supplied value. Detail endpoint (deliberate drill-in) returns the full value.","x-action-risk":"low","x-idempotent":true,"x-reversible":true,"x-dry-run-supported":true,"x-required-scope":"payroll:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"first_name":{"type":"string"},"last_name":{"type":"string"},"personnummer_masked":{"type":"string"},"employment_type":{"type":"string","enum":["employee","company_owner","board_member"]},"employment_start":{"type":"string"},"employment_end":{"type":"string"},"employment_degree":{"type":"number"},"salary_type":{"type":"string","enum":["monthly","hourly"]},"monthly_salary":{"type":"number"},"hourly_rate":{"type":"number"},"tax_table_number":{"type":"number"},"tax_column":{"type":"number"},"tax_municipality":{"type":"string"},"is_sidoinkomst":{"type":"boolean"},"f_skatt_status":{"type":"string","enum":["a_skatt","f_skatt","fa_skatt","not_verified"]},"vacation_rule":{"type":"string"},"vacation_days_per_year":{"type":"number"},"is_active":{"type":"boolean"},"created_at":{"type":"string"}},"required":["id","first_name","last_name","personnummer_masked","employment_type","employment_start","employment_end","employment_degree","salary_type","monthly_salary","hourly_rate","tax_table_number","tax_column","tax_municipality","is_sidoinkomst","f_skatt_status","vacation_rule","vacation_days_per_year","is_active","created_at"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/employees/{id}":{"get":{"operationId":"employees.get","summary":"Get a single employee.","description":"Returns the full employee record including the 12-digit personnummer, bank details, tax configuration, and contact info. This is the deliberate drill-in for an id you already know: list calls mask personnummer.\n\n**Use when:** You have an employee id and need every field (tax table, bank account, vacation rule): typically to render an edit form or to construct a payroll calculation input.\n**Do not use for:** Rosters or pickers (use the list endpoint: personnummer is masked there).\n\n**Pitfalls:**\n- The response includes the full personnummer. Treat it as a national identifier (GDPR Art.5(1)(c)): do not propagate it to logs or external systems beyond what your integration strictly requires.\n- Inactive (soft-deleted) employees are returned by the detail endpoint; check `is_active` if your flow should skip them.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"payroll:read","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"first_name":{"type":"string"},"last_name":{"type":"string"},"personnummer":{"type":"string"},"employment_type":{"type":"string","enum":["employee","company_owner","board_member"]},"employment_start":{"type":"string"},"employment_end":{"type":"string"},"employment_degree":{"type":"number"},"salary_type":{"type":"string","enum":["monthly","hourly"]},"monthly_salary":{"type":"number"},"hourly_rate":{"type":"number"},"tax_table_number":{"type":"number"},"tax_column":{"type":"number"},"tax_municipality":{"type":"string"},"is_sidoinkomst":{"type":"boolean"},"f_skatt_status":{"type":"string","enum":["a_skatt","f_skatt","fa_skatt","not_verified"]},"clearing_number":{"type":"string"},"bank_account_number":{"type":"string"},"vacation_rule":{"type":"string"},"vacation_days_per_year":{"type":"number"},"semestertillagg_rate":{"type":"number"},"email":{"type":"string"},"phone":{"type":"string"},"address_line1":{"type":"string"},"postal_code":{"type":"string"},"city":{"type":"string"},"vaxa_stod_eligible":{"type":"boolean"},"vaxa_stod_start":{"type":"string"},"vaxa_stod_end":{"type":"string"},"default_dimensions":{},"is_active":{"type":"boolean"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","first_name","last_name","personnummer","employment_type","employment_start","employment_end","employment_degree","salary_type","monthly_salary","hourly_rate","tax_table_number","tax_column","tax_municipality","is_sidoinkomst","f_skatt_status","clearing_number","bank_account_number","vacation_rule","vacation_days_per_year","semestertillagg_rate","email","phone","address_line1","postal_code","city","vaxa_stod_eligible","vaxa_stod_start","vaxa_stod_end","default_dimensions","is_active","created_at","updated_at"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}},"patch":{"operationId":"employees.update","summary":"Update an employee.","description":"Partial update of an employee. Only the fields supplied in the body are changed. Supports ?dry_run=true to validate the merged record without committing. Personnummer changes are NOT permitted via this endpoint: the natural-person identity is immutable post-creation.\n\n**Use when:** You need to change tax configuration, bank details, salary amount, or contact info on an existing employee.\n**Do not use for:** Changing personnummer (not supported: create a new employee if the natural-person identity changes, which is a rare edge case). Soft-deleting (use DELETE).\n\n**Pitfalls:**\n- personnummer in the body is ignored by this endpoint. To change it you must DELETE and recreate.\n- salary_type changes require the matching salary field in the same request: switching to monthly without monthly_salary returns 400.\n- tax_table_number changes only take effect on future salary runs; runs already in `review` or beyond use a frozen snapshot.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":true,"x-required-scope":"payroll:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"first_name":{"type":"string"},"last_name":{"type":"string"},"employment_type":{"type":"string","enum":["employee","company_owner","board_member"]},"employment_start":{"type":"string"},"employment_end":{"type":"string"},"employment_degree":{"type":"number"},"salary_type":{"type":"string","enum":["monthly","hourly"]},"monthly_salary":{"type":"number"},"hourly_rate":{"type":"number"},"tax_table_number":{"type":"number"},"tax_column":{"type":"number"},"tax_municipality":{"type":"string"},"is_sidoinkomst":{"type":"boolean"},"f_skatt_status":{"type":"string","enum":["a_skatt","f_skatt","fa_skatt","not_verified"]},"clearing_number":{"type":"string"},"bank_account_number":{"type":"string"},"vacation_rule":{"type":"string"},"vacation_days_per_year":{"type":"number"},"semestertillagg_rate":{"type":"number"},"email":{"type":"string"},"phone":{"type":"string"},"address_line1":{"type":"string"},"postal_code":{"type":"string"},"city":{"type":"string"},"vaxa_stod_eligible":{"type":"boolean"},"vaxa_stod_start":{"type":"string"},"vaxa_stod_end":{"type":"string"},"default_dimensions":{},"is_active":{"type":"boolean"},"created_at":{"type":"string"},"updated_at":{"type":"string"},"personnummer_masked":{"type":"string"}},"required":["id","first_name","last_name","employment_type","employment_start","employment_end","employment_degree","salary_type","monthly_salary","hourly_rate","tax_table_number","tax_column","tax_municipality","is_sidoinkomst","f_skatt_status","clearing_number","bank_account_number","vacation_rule","vacation_days_per_year","semestertillagg_rate","email","phone","address_line1","postal_code","city","vaxa_stod_eligible","vaxa_stod_start","vaxa_stod_end","default_dimensions","is_active","created_at","updated_at","personnummer_masked"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}},"delete":{"operationId":"employees.delete","summary":"Soft-delete an employee.","description":"Sets `is_active=false`. The row is preserved because past salary runs reference it via salary_run_employees and those verifikationer are räkenskapsinformation under BFL 7 kap (BFL retention attaches to the verifikationer themselves, not strictly to the personnummer attribute on the master row). Hard delete is never exposed.\n\n**Use when:** An employee has left the company and should no longer appear in active rosters or default to new salary runs.\n**Do not use for:** Reactivating later (PATCH `is_active=true` instead). Hard-deleting (not supported: retention).\n\n**Pitfalls:**\n- Idempotent: deleting an already-inactive employee returns 204 No Content (the same as the first call).\n- The row is NOT removed from the database: re-creating with the same personnummer returns 409 EMPLOYEE_DUPLICATE_PERSONNUMMER even after soft-delete.\n- Past salary runs still reference this employee; their data continues to surface in GET /salary-runs/{id} and SIE exports.","x-action-risk":"low","x-idempotent":true,"x-reversible":true,"x-dry-run-supported":true,"x-required-scope":"payroll:write","responses":{"204":{"description":"No Content"},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/salary-runs":{"get":{"operationId":"salary-runs.list","summary":"List salary runs.","description":"Returns salary runs in created-first order with their lifecycle status (draft|review|approved|paid|booked|corrected) and denormalised totals. Filters: ?period_year=YYYY, ?status=draft.\n\n**Use when:** You need an overview of payroll activity: for building a list view, finding the current open run, or resolving a salary_run_id before invoking a lifecycle verb.\n**Do not use for:** Per-employee details (those live on the detail endpoint). Salary journal report (use GET /reports/salary-journal in Phase 5 PR-3).\n\n**Pitfalls:**\n- A company has at most one salary run per (period_year, period_month). The unique constraint is at the DB layer.\n- Totals are denormalised: they are 0 until POST /calculate runs.\n- `corrected` status is reached via the internal /correct route (not yet exposed on v1): Phase 5 PR-1 ships create/calculate/approve/mark-paid/book/generate-agi only.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"payroll:read","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"period_year":{"type":"number"},"period_month":{"type":"number"},"payment_date":{"type":"string"},"status":{"type":"string","enum":["draft","review","approved","paid","booked","corrected"]},"voucher_series":{"type":"string"},"total_gross":{"type":"number"},"total_tax":{"type":"number"},"total_net":{"type":"number"},"total_avgifter":{"type":"number"},"total_employer_cost":{"type":"number"},"agi_generated_at":{"type":"string"},"agi_submitted_at":{"type":"string"},"approved_at":{"type":"string"},"paid_at":{"type":"string"},"booked_at":{"type":"string"},"created_at":{"type":"string"}},"required":["id","period_year","period_month","payment_date","status","voucher_series","total_gross","total_tax","total_net","total_avgifter","total_employer_cost","agi_generated_at","agi_submitted_at","approved_at","paid_at","booked_at","created_at"],"additionalProperties":false}},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}},"post":{"operationId":"salary-runs.create","summary":"Create a salary run.","description":"Creates a draft salary run for the given period (period_year, period_month). The run starts empty: add employees via the internal /salary/runs/{id}/employees endpoints, then POST /salary-runs/{id}/calculate. Requires Idempotency-Key. Dry-runnable.\n\n**Use when:** You are starting a new month's payroll. Use dry-run first to validate the period + voucher_series choice without committing.\n**Do not use for:** Adding employees to an existing run (that is a separate surface: see internal /salary/runs/{id}/employees for Phase 5 PR-1; promoting it to v1 is deferred to a follow-up).\n\n**Pitfalls:**\n- Idempotency-Key is mandatory.\n- Duplicate (period_year, period_month) for the same company returns 409 SALARY_RUN_DUPLICATE_PERIOD.\n- period_month is 1-12. The DB CHECK enforces this: a 0 or 13 returns 400 VALIDATION_ERROR before reaching the DB.\n- voucher_series defaults to \"A\". If the company uses a dedicated salary voucher series, set it explicitly.\n- A newly-created run has no employees: :calculate without employees returns 400 SALARY_RUN_NO_EMPLOYEES.","x-action-risk":"low","x-idempotent":true,"x-reversible":true,"x-dry-run-supported":true,"x-required-scope":"payroll:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"period_year":{"type":"number"},"period_month":{"type":"number"},"payment_date":{"type":"string"},"status":{"type":"string","enum":["draft","review","approved","paid","booked","corrected"]},"voucher_series":{"type":"string"},"total_gross":{"type":"number"},"total_tax":{"type":"number"},"total_net":{"type":"number"},"total_avgifter":{"type":"number"},"total_employer_cost":{"type":"number"},"agi_generated_at":{"type":"string"},"agi_submitted_at":{"type":"string"},"approved_at":{"type":"string"},"paid_at":{"type":"string"},"booked_at":{"type":"string"},"created_at":{"type":"string"},"notes":{"type":"string"},"calculation_params":{},"updated_at":{"type":"string"}},"required":["id","period_year","period_month","payment_date","status","voucher_series","total_gross","total_tax","total_net","total_avgifter","total_employer_cost","agi_generated_at","agi_submitted_at","approved_at","paid_at","booked_at","created_at","notes","calculation_params","updated_at"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/salary-runs/{id}":{"get":{"operationId":"salary-runs.get","summary":"Get a salary run.","description":"Returns the salary run's lifecycle state, denormalised totals (gross/tax/net/avgifter/vacation/employer_cost), and references to the journal entries it produced (once :book has run).\n\n**Use when:** You have a salary_run_id and need its current status: typically to decide which lifecycle verb to call next, or to display the run header in a UI.\n**Do not use for:** Per-employee breakdown (Phase 5 PR-1 does not expose the per-employee endpoint on v1; use the internal /api/salary/runs/{id} for that today). Salary journal report: use GET /reports/salary-journal in Phase 5 PR-3.\n\n**Pitfalls:**\n- salary_entry_id / avgifter_entry_id / vacation_entry_id are null until POST /book has run. They reference the journal_entries table.\n- total_* fields are 0 until POST /calculate has run.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"payroll:read","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"period_year":{"type":"number"},"period_month":{"type":"number"},"payment_date":{"type":"string"},"status":{"type":"string","enum":["draft","review","approved","paid","booked","corrected"]},"voucher_series":{"type":"string"},"total_gross":{"type":"number"},"total_tax":{"type":"number"},"total_net":{"type":"number"},"total_avgifter":{"type":"number"},"total_vacation_accrual":{"type":"number"},"total_employer_cost":{"type":"number"},"salary_entry_id":{"type":"string"},"avgifter_entry_id":{"type":"string"},"vacation_entry_id":{"type":"string"},"agi_generated_at":{"type":"string"},"agi_submitted_at":{"type":"string"},"calculation_params":{},"approved_by":{"type":"string"},"approved_at":{"type":"string"},"paid_at":{"type":"string"},"booked_at":{"type":"string"},"booked_by":{"type":"string"},"notes":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","period_year","period_month","payment_date","status","voucher_series","total_gross","total_tax","total_net","total_avgifter","total_vacation_accrual","total_employer_cost","salary_entry_id","avgifter_entry_id","vacation_entry_id","agi_generated_at","agi_submitted_at","calculation_params","approved_by","approved_at","paid_at","booked_at","booked_by","notes","created_at","updated_at"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}},"patch":{"operationId":"salary-runs.update","summary":"Update a draft salary run.","description":"Updates payment_date, voucher_series, or notes on a draft salary run. ONLY allowed when status === \"draft\": once :calculate has advanced the run to review, these fields are frozen because they feed into the verifikation that :book will eventually post.\n\n**Use when:** You created a draft, then noticed payment_date should be different (e.g. moved from the 25th to the 23rd) before running :calculate.\n**Do not use for:** Changing period_year / period_month (immutable: DELETE the draft and create a new one). Modifying employees in the run (not in v1 PR-1 scope).\n\n**Pitfalls:**\n- Returns 400 SALARY_RUN_PATCH_NOT_DRAFT if status !== \"draft\".\n- period_year + period_month are immutable post-create.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":true,"x-required-scope":"payroll:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"period_year":{"type":"number"},"period_month":{"type":"number"},"payment_date":{"type":"string"},"status":{"type":"string","enum":["draft","review","approved","paid","booked","corrected"]},"voucher_series":{"type":"string"},"total_gross":{"type":"number"},"total_tax":{"type":"number"},"total_net":{"type":"number"},"total_avgifter":{"type":"number"},"total_vacation_accrual":{"type":"number"},"total_employer_cost":{"type":"number"},"salary_entry_id":{"type":"string"},"avgifter_entry_id":{"type":"string"},"vacation_entry_id":{"type":"string"},"agi_generated_at":{"type":"string"},"agi_submitted_at":{"type":"string"},"calculation_params":{},"approved_by":{"type":"string"},"approved_at":{"type":"string"},"paid_at":{"type":"string"},"booked_at":{"type":"string"},"booked_by":{"type":"string"},"notes":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","period_year","period_month","payment_date","status","voucher_series","total_gross","total_tax","total_net","total_avgifter","total_vacation_accrual","total_employer_cost","salary_entry_id","avgifter_entry_id","vacation_entry_id","agi_generated_at","agi_submitted_at","calculation_params","approved_by","approved_at","paid_at","booked_at","booked_by","notes","created_at","updated_at"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}},"delete":{"operationId":"salary-runs.delete","summary":"Delete a draft salary run.","description":"Hard-deletes a salary run. ONLY allowed when status === \"draft\": once the run has calculated numbers or posted a verifikation, BFL 5 kap immutability applies and storno is the only correction path. CASCADE deletes salary_run_employees and salary_line_items.\n\n**Use when:** You created a run by mistake or want to recreate it with different period_month. Only draft runs can be deleted.\n**Do not use for:** Reverting a booked run (use the internal /correct flow; v1 promotion deferred). Hiding a run from listings (no soft-delete on this table: drafts are truly removed).\n\n**Pitfalls:**\n- Returns 400 SALARY_RUN_DELETE_NOT_DRAFT for any status other than draft.\n- Hard delete: the salary_run_employees + salary_line_items rows cascade away.\n- Idempotent in the absent-row sense: DELETE on a non-existent id returns 404 SALARY_RUN_NOT_FOUND rather than re-emitting a deletion event.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":true,"x-required-scope":"payroll:write","responses":{"204":{"description":"No Content"},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/salary-runs/{id}/calculate":{"post":{"operationId":"salary-runs.calculate","summary":"Calculate a draft salary run and advance it to review.","description":"Runs the per-employee payroll calculation (tax withholding, employer contributions, vacation accrual) for every employee on a draft run, persists the line items + run totals + calculation_params snapshot, then promotes status from draft to review in a single atomic verb. Returns the updated run plus a `warnings` array surfacing non-blocking issues (Skatteverket tax-table fallback, läkarintyg day-8 transition, Försäkringskassan day-15 transition, F-skatt not-verified employees). Strict-mode: any failure (validation, tax-table unavailable, DB error) aborts before the status flip: the run stays in draft.\n\n**Use when:** You have a draft salary run with employees added and want to compute the numbers + freeze them for approval. This is the first lifecycle verb after creating a run.\n**Do not use for:** re-running a salary run already in review or later (only `draft` is accepted: call POST :correct in Phase 5 PR-3 once that ships to revise a booked run). Adding employees to the run (that surface is not yet on v1; use the dashboard).\n\n**Pitfalls:**\n- Run must be in `draft` status: calculate on a non-draft run returns 400 SALARY_RUN_CALCULATE_NOT_DRAFT.\n- Salary run must have at least one employee: empty runs return 400 SALARY_RUN_NO_EMPLOYEES.\n- If Skatteverket's tax-table API is down and local fallback is missing the required table, calculate returns 503 SALARY_RUN_TAX_TABLE_MISSING. Retry is safe; the operation is idempotent at the helper level.\n- F-skatt \"not_verified\" employees produce a non-blocking warning; an integrator should treat the warning as a hard signal that withholding will be wrong until F-skatt is verified.\n- Warnings about tax-table fallback or läkarintyg / FK day-15 transitions are non-blocking; the run still advances to review. Surface them to a human reviewer before calling :approve.","x-action-risk":"medium","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":true,"x-required-scope":"payroll:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"status":{"const":"review"},"period_year":{"type":"number"},"period_month":{"type":"number"},"total_gross":{"type":"number"},"total_tax":{"type":"number"},"total_net":{"type":"number"},"total_avgifter":{"type":"number"},"total_employer_cost":{"type":"number"},"warnings":{"type":"array","items":{"type":"string"}}},"required":["id","status","period_year","period_month","total_gross","total_tax","total_net","total_avgifter","total_employer_cost","warnings"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/salary-runs/{id}/approve":{"post":{"operationId":"salary-runs.approve","summary":"Approve a reviewed salary run.","description":"Advances a salary run from `review` to `approved` after validating every employee has the data required for the payment step (bank account + clearing number for the bank transfer) and the booking step (`calculation_breakdown` proves `:calculate` ran). Records the approving user + timestamp. Strict-mode: validation errors return a complete list rather than failing on the first one.\n\n**Use when:** You have a salary run in `review` status and want to authorize it for payment. This is the human (or agent) signoff step before money moves; the verifikation is still pending and won't exist until `:book` runs.\n**Do not use for:** Posting journal entries (use `:book` after `:mark-paid`). Reverting an approval (the lifecycle has no `:unapprove`: call `:correct` once the run is booked if you need to undo).\n\n**Pitfalls:**\n- Run must be in `review`: non-`review` runs return 400 SALARY_RUN_APPROVE_NOT_REVIEW.\n- Every employee on the run needs a `clearing_number` + `bank_account_number`. Missing bank details return 400 SALARY_RUN_APPROVE_VALIDATION_FAILED with the per-employee list.\n- Every employee on the run needs `calculation_breakdown` populated. If you skipped `:calculate` somehow, approve fails.\n- Employees without email get a non-blocking warning (lönebesked can't be sent automatically).\n- No period-lock check here: that lives on `:book` where the verifikation is posted. An agent can approve a run whose payment date falls in a now-locked period; `:book` will later refuse.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":true,"x-required-scope":"payroll:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"status":{"const":"approved"},"approved_at":{"type":"string"},"approved_by":{"type":"string"},"warnings":{"type":"array","items":{"type":"string"}}},"required":["id","status","approved_at","approved_by","warnings"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/salary-runs/{id}/mark-paid":{"post":{"operationId":"salary-runs.mark-paid","summary":"Mark an approved salary run as paid.","description":"Advances a salary run from `approved` to `paid` and stamps `paid_at`. This is the state-change verb after the bank transfer (or autogiro file) has been processed; it does NOT initiate payment, and does NOT post journal entries (use `:book` after this for that).\n\n**Use when:** You've confirmed the salary payment hit employee bank accounts and want to advance the run's lifecycle so `:book` can post the verifikation.\n**Do not use for:** Initiating the actual bank transfer (the v1 API does not yet expose payment-file generation; use the dashboard's payment-file endpoints). Posting journal entries (use `:book`). Reverting a paid run (no `:unpaid` exists: call `:correct` once booked if you need to undo).\n\n**Pitfalls:**\n- Run must be in `approved`: non-`approved` runs return 400 SALARY_RUN_MARK_PAID_NOT_APPROVED.\n- paid_at is set server-side to the current UTC timestamp; the API does not accept a body-supplied date to keep BFL audit clean.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":true,"x-required-scope":"payroll:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"status":{"const":"paid"},"paid_at":{"type":"string"}},"required":["id","status","paid_at"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/salary-runs/{id}/book":{"post":{"operationId":"salary-runs.book","summary":"Post the verifikationer for a paid salary run.","description":"Creates 2-4 journal entries (1: salary brutto/tax/net; 2: arbetsgivaravgifter; 3 if applicable: semesterlöneskuld accrual; 4 if applicable: pension + SLP from löneväxling), then advances status `paid` → `booked` with all the entry IDs recorded on the salary_runs row. Strict-mode: any engine failure aborts BEFORE the status flip: the run stays in `paid` so the caller can fix the cause (locked period, missing BAS account, etc.) and retry.\n\n**Use when:** You've marked a salary run as paid and want to post the BFL-required verifikationer. This is the final lifecycle verb before AGI generation; after :book, the run can no longer be edited and corrections must use the (forthcoming) `:correct` verb.\n**Do not use for:** Posting salary entries outside the salary-run lifecycle (use POST /journal-entries directly). Re-booking an already-booked run (returns 400 SALARY_RUN_BOOK_NOT_PAID).\n\n**Pitfalls:**\n- Run must be in `paid`: non-`paid` runs return 400 SALARY_RUN_BOOK_NOT_PAID.\n- payment_date must fall in an open fiscal period: locked period returns 400 PERIOD_LOCKED with `fiscal_period_id` and a hint of what unlock action is needed.\n- BFL 5 kap immutability: once `:book` succeeds the verifikationer cannot be edited or deleted. Corrections require `:correct` (Phase 5 PR-3) which does a storno-then-rebook.\n- The salary verifikation is the primary one; its voucher_number appears in the response audit block. The avgifter, vacation, and pension entries get separate voucher numbers (returned as `entry_ids`).\n- Strict-mode: if the engine fails partway, the salary_runs row stays in `paid`. There is no \"partial booking\": the engine either commits all entries or the entire booking fails.","x-action-risk":"high","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":true,"x-required-scope":"payroll:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"status":{"const":"booked"},"booked_at":{"type":"string"},"booked_by":{"type":"string"},"salary_entry_id":{"type":"string"},"avgifter_entry_id":{"type":"string"},"vacation_entry_id":{"type":"string"},"pension_entry_id":{"type":"string"},"entry_ids":{"type":"array","items":{"type":"string"}}},"required":["id","status","booked_at","booked_by","salary_entry_id","avgifter_entry_id","vacation_entry_id","pension_entry_id","entry_ids"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/salary-runs/{id}/generate-agi":{"post":{"operationId":"salary-runs.generate-agi","summary":"Generate the Skatteverket AGI XML for a salary run.","description":"Generates the arbetsgivardeklaration-på-individnivå XML for the run (HU section + per-employee IU + Frånvarouppgift for VAB/parental), upserts the agi_declarations row (correction-aware), stamps salary_runs.agi_generated_at, emits `agi.generated`, and auto-completes the `arbetsgivardeklaration` deadline. Returns the XML as a string field in the v1 envelope: agents extract `data.xml` and forward to Skatteverket directly (Mina Sidor upload or via a connected extension).\n\n**Use when:** You've reviewed (or approved / paid / booked) a salary run and need to file AGI with Skatteverket. The Skatteverket filing deadline is the 12th of the following month (17th in Jan / Aug for companies ≤40 MSEK turnover).\n**Do not use for:** Submitting the AGI to Skatteverket: this endpoint only generates and persists the XML. Submission is a separate flow via the (optional) `skatteverket` extension.\n\n**Pitfalls:**\n- Run status must be one of review, approved, paid, booked, corrected: `draft` returns 400 AGI_GENERATE_NOT_BOOKABLE.\n- Generating AGI from a `review`-status run risks submitting figures that will change at `:approve`. The dashboard allows this for flexibility; agents should prefer `approved+` unless an early-warning workflow specifically wants the preview.\n- Subsequent calls for the same period UPDATE the agi_declarations row (is_correction=true) and overwrite the XML. The FK570 specifikationsnummer stays consistent per employee: different number = new record per Skatteverket spec.\n- AGI_INCOMPLETE_DATA returns 400 when company contact info is missing (org_number, contact name, phone, email). Fix via /settings/company before retrying.\n- The XML content is räkenskapsinformation: BFL 7 kap retention applies. The agi_declarations row is never auto-deleted.","x-action-risk":"medium","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"payroll:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"agi_declaration_id":{"type":"string"},"period_year":{"type":"number"},"period_month":{"type":"number"},"employee_count":{"type":"number"},"is_correction":{"type":"boolean"},"totals":{"type":"object","properties":{"totalTax":{"type":"number"},"totalAvgifterBasis":{"type":"number"},"totalAvgifterAmount":{"type":"number"},"totalSjuklonekostnad":{"type":"number"},"avgifterByCategory":{}},"required":["totalTax","totalAvgifterBasis","totalAvgifterAmount","totalSjuklonekostnad","avgifterByCategory"],"additionalProperties":false},"xml":{"type":"string"},"xml_filename":{"type":"string"}},"required":["agi_declaration_id","period_year","period_month","employee_count","is_correction","totals","xml","xml_filename"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/reports/trial-balance":{"get":{"operationId":"reports.trial-balance","summary":"Trial balance (huvudboksrapport) for a fiscal period.","description":"Returns the per-account opening balance + period debit/credit + closing balance plus run-level totals and an `isBalanced` flag. The numbers come from the same `lib/reports/trial-balance.ts` generator the dashboard uses.\n\n**Use when:** You need a snapshot of every active account's movement during a period: typically the first report an accountant checks before running balance sheet or income statement.\n**Do not use for:** Reconciliation against AR/AP (use /reports/ar-ledger or /supplier-ledger). Specific account drill-in (use /reports/general-ledger with account_from/account_to filters).\n\n**Pitfalls:**\n- `period_id` is required as a query parameter.\n- `isBalanced=false` means the period has unbalanced postings: a data-integrity red flag. The lib generator rounds at the source so a true imbalance is rare; investigate immediately.\n- Closed/locked periods are still queryable: the report is read-only.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"reports:read","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"rows":{"type":"array","items":{"type":"object","properties":{"account":{"type":"string"},"account_name":{"type":"string"},"opening_balance":{"type":"number"},"period_debit":{"type":"number"},"period_credit":{"type":"number"},"closing_balance":{"type":"number"}},"required":["account","account_name","opening_balance","period_debit","period_credit","closing_balance"],"additionalProperties":false}},"totalDebit":{"type":"number"},"totalCredit":{"type":"number"},"isBalanced":{"type":"boolean"}},"required":["rows","totalDebit","totalCredit","isBalanced"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/reports/balance-sheet":{"get":{"operationId":"reports.balance-sheet","summary":"Balance sheet (balansräkning) for a fiscal period.","description":"Returns assets / liabilities / equity grouped into BAS sections, with the period's opening and closing balances. Sums match the income statement for the same period; the closing equity flows into next period's opening balance.\n\n**Use when:** You need the company's balance position at period end: typically for management reporting, year-end review, or the K2/K3 årsredovisning uppställningsform.\n**Do not use for:** Per-account drill-down (use /reports/general-ledger). Net result for the period (use /reports/income-statement).\n\n**Pitfalls:**\n- `period_id` is required.\n- Balance sheet equity includes the period's computed result: recalculation happens on every call, so a freshly-posted entry is reflected immediately (no caching).","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"reports:read","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/reports/income-statement":{"get":{"operationId":"reports.income-statement","summary":"Income statement (resultatrapport) for a fiscal period.","description":"Returns the period's revenue and expenses grouped by BAS class with subtotals (gross margin, operating result, net result). The net result flows into the balance-sheet equity for the same period.\n\n**Use when:** You need the company's profit/loss for a period: month-end management reporting, K2/K3 årsredovisning resultaträkning, or feeding KPI dashboards.\n**Do not use for:** Per-account drill (use /reports/general-ledger). VAT figures (use /reports/vat-declaration). Balance position (use /reports/balance-sheet).\n\n**Pitfalls:**\n- `period_id` is required.\n- Net result on the income statement equals the period's equity-line delta on the balance sheet: they're derived from the same posted entries.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"reports:read","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/reports/general-ledger":{"get":{"operationId":"reports.general-ledger","summary":"General ledger (huvudbok) for a fiscal period.","description":"Returns every posted journal line in the period grouped by account, with opening / running / closing balances. Supports optional `account_from` and `account_to` query parameters to limit the report to an account range (e.g. ?account_from=3000&account_to=3999 for revenue-only).\n\n**Use when:** You're reconciling a specific account or range (bank account drilldown, revenue audit, expense investigation) and need every voucher-line that hit the account.\n**Do not use for:** Period totals only (use /reports/trial-balance). Specific transaction lookup (use /journal-entries/{id}).\n\n**Pitfalls:**\n- `period_id` is required.\n- Account ranges are inclusive on both bounds. `account_from=3000` includes 3000; `account_to=3999` includes 3999.\n- Lines with `status != 'posted'` (drafts, reversed) are excluded.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"reports:read","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/reports/journal-register":{"get":{"operationId":"reports.journal-register","summary":"Journal register (verifikationsregister) for a fiscal period.","description":"Returns every committed journal entry in the period with its voucher number, date, description, and complete debit/credit line set. The canonical compliance report: what an accountant or Skatteverket audit would pull as proof of every booking.\n\n**Use when:** You need the BFL-required register of all verifikationer for a period: typically for an audit, year-end review, or feeding an external accountant's tooling.\n**Do not use for:** Per-account drilldown (use /reports/general-ledger). Aggregate totals only (use /reports/trial-balance).\n\n**Pitfalls:**\n- `period_id` is required.\n- Output includes every line of every entry: large periods produce large responses. Consider paginating client-side or filtering by date range via /journal-entries list if you only need a slice.\n- Reversed entries appear with status `reversed`; the original they reversed also remains.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"reports:read","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/reports/vat-declaration":{"get":{"operationId":"reports.vat-declaration","summary":"Swedish VAT declaration (momsdeklaration) for a period.","description":"Computes momsdeklaration rutor for the given period_type / year / period. The result includes ruta 05 (domestic taxable sales), 10-12 (output VAT 25/12/6%), 20-24 (EU acquisitions of goods + tax on services from EU/non-EU), 30-32 (reverse-charge output VAT 25/12/6%), 39 (export), 40 (EU-services / momsfri försäljning), 48 (input VAT), 50 (import beskattningsunderlag), 60-62 (calculated output VAT on imports 25/12/6%), and 49 (moms att betala/återfå: the bottom line). Mapping rules match SKV 4700.\n\n**Use when:** Submitting momsdeklaration to Skatteverket, reconciling VAT balances at month/quarter end, or building a VAT-payable dashboard.\n**Do not use for:** Specific transaction VAT lookups (use /transactions/{id}). Period-mismatch reconciliation (use /reports/general-ledger filtered to 26xx accounts).\n\n**Pitfalls:**\n- `period_type` (monthly|quarterly|yearly), `year`, and `period` are all required.\n- For monthly: period is 1-12. For quarterly: period is 1-4. For yearly: period is 1.\n- `accounting_method` defaults to accrual (faktureringsmetoden); pass cash for kontantmetoden to honor the VAT-on-payment rule per ML 15 kap 8-11 §§ (ML 2023:200, which replaced ML 1994:200 on 1 July 2023: the prior ML 13 kap reference is outdated).\n- Output ruta 49 = (10+11+12+30+31+32+60+61+62) − 48. Positive = pay; negative = refund.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"reports:read","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/reports/monthly-breakdown":{"get":{"operationId":"reports.monthly-breakdown","summary":"Income statement broken down by month for a fiscal period.","description":"Returns revenue + expenses + net result per calendar month inside the fiscal period. The sum across all months equals the period's full income-statement totals.\n\n**Use when:** Building a trend chart, computing rolling KPIs, or producing a månadsrapport for management.\n**Do not use for:** Single-month snapshot only (call /reports/income-statement with a month-sized period). Cash flow analysis (a dedicated cash-flow report is not yet on v1).\n\n**Pitfalls:**\n- `period_id` is required.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"reports:read","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/reports/ar-ledger":{"get":{"operationId":"reports.ar-ledger","summary":"AR ledger: unpaid customer invoices with aging.","description":"Returns the customer-receivable ledger as of `as_of_date` (defaults to today). Each customer entry includes outstanding invoices grouped into aging buckets (0-30, 31-60, 61-90, 90+ days). Reconciles against BAS 1510.\n\n**Use when:** Cash collection dashboards, dunning workflows, end-of-period reconciliation against the 1510 trial-balance figure.\n**Do not use for:** Listing all invoices regardless of status (use /invoices). Sending dunning emails (the v1 surface does not yet expose dunning).\n\n**Pitfalls:**\n- `as_of_date` is optional; format `YYYY-MM-DD`. Defaults to today (UTC).\n- Only invoices in `sent`/`overdue`/`partially_paid` status appear. Drafts and credited invoices are excluded.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"reports:read","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/reports/supplier-ledger":{"get":{"operationId":"reports.supplier-ledger","summary":"Supplier ledger: unpaid supplier invoices with aging.","description":"Returns the supplier-payable ledger as of `as_of_date` (defaults to today). Each supplier entry includes outstanding invoices grouped into aging buckets. Reconciles against BAS 2440.\n\n**Use when:** AP workflow dashboards, due-date prioritisation, reconciliation against the 2440 trial-balance figure.\n**Do not use for:** Listing all supplier invoices regardless of status (use /supplier-invoices). Initiating payment (the v1 surface does not expose payment files yet).\n\n**Pitfalls:**\n- `as_of_date` is optional; format `YYYY-MM-DD`. Defaults to today (UTC).\n- Only invoices with outstanding `remaining_amount > 0` appear. Credited and fully-paid invoices are excluded.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"reports:read","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/reports/continuity-check":{"get":{"operationId":"reports.continuity-check","summary":"IB/UB continuity check: opening balances match prior closing.","description":"Validates that the target period's opening balances (IB) equal the prior period's closing balances (UB). The requirement derives from BFL 5 kap (löpande bokföring), BFNAR 2013:2 (systemdokumentation/behandlingshistorik), and the SIE4 spec's core invariant that #IB(year N) must equal #UB(year N-1). Returns per-account discrepancies so an operator can rectify them before period close.\n\n**Use when:** Before locking or closing a period, or as part of an automated year-end readiness gate. Any discrepancy is a hard data-integrity issue.\n**Do not use for:** Computing balances (use /reports/balance-sheet or /reports/trial-balance). Closing the period (POST /fiscal-periods/{id}/close).\n\n**Pitfalls:**\n- `period_id` is required.\n- A non-zero discrepancy means IB ≠ prior UB and indicates the opening-balance entry was edited or the prior period was changed after close. Investigate before posting any new entries.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"reports:read","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/reports/salary-journal":{"get":{"operationId":"reports.salary-journal","summary":"Salary journal (lönejournal) for a year and optional month range.","description":"Returns per-employee salary figures (gross / tax / net / avgifter / vacation accrual) summed across booked salary runs in `year`. Optional `month_from` and `month_to` limit the window. The output mirrors the dashboard's lönejournal export. ⚠️ KU (kontrolluppgift) preparation requires the FULL annual paid amount per employee: if any salary runs are in paid-but-unbooked state at KU time, generating KU from this report will understate wages (an SFL obligation breach). Confirm all paid runs are booked before using this report for KU.\n\n**Use when:** Year-end KU preparation, employee comp reviews, reconciliation against the 7xxx wage accounts.\n**Do not use for:** Per-run drill-down (use /salary-runs/{id} once the per-employee endpoint ships). AGI declarations (POST /salary-runs/{id}/generate-agi).\n\n**Pitfalls:**\n- `year` is required (integer 2020-2100).\n- Only `booked` salary runs are included: `draft`/`review`/`approved`/`paid` runs are excluded as they aren't legally final.\n- `paid`-but-unbooked runs are EXCLUDED. This means the report reconciles cleanly against BAS 7xxx (the ledger), but an AGI-vs-ledger cross-check will show a gap until the run is booked. The AGI is filed at `approved`/`paid` (Phase 5 PR-2 allows it from `review`), so reconciling AGI against this report requires waiting until every paid run is also booked.\n- month_from/month_to are 1-12 inclusive.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"payroll:read","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/reports/avgifter-basis":{"get":{"operationId":"reports.avgifter-basis","summary":"Annual arbetsgivaravgifter basis per employee.","description":"Returns the annual avgifter basis per employee for `year`, summed across booked salary runs. Each row shows the basis, applied rate, and computed avgifter amount: useful for reconciling against monthly AGI filings (HU sum across the year).\n\n**Use when:** Annual reconciliation between the AGI declarations and the bookkeeping (BAS 7510). Year-end audit prep.\n**Do not use for:** Real-time AGI generation (POST /salary-runs/{id}/generate-agi). Per-run breakdown (use /reports/salary-journal).\n\n**Pitfalls:**\n- `year` is required.\n- Only `booked` runs are included.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"payroll:read","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/reports/vacation-liability":{"get":{"operationId":"reports.vacation-liability","summary":"Vacation liability (semesterlöneskuld) per employee at year-end.","description":"Returns per-employee semesterlöneskuld balances as of year-end based on their vacation_rule (procentregeln / sammaloneregeln) and accrued days. For employees on procentregeln or sammaloneregeln the row total contributes to the BAS 2920 closing balance. Employees on `none` or `semesterersattning` are excluded because their cost is expensed immediately (no balance-sheet accrual): the BAS 2920 reconciliation against this report is therefore CORRECT whether or not the company has semesterersättning employees, since those employees contribute zero to both the report and the 2920 balance. Feeds the K2/K3 årsredovisning notes.\n\n**Use when:** Year-end reconciliation between the accrued liability on 2920 and the per-employee detail. Audit prep.\n**Do not use for:** Real-time accrual posting (handled per salary run). Vacation request management (not in scope for v1).\n\n**Pitfalls:**\n- `year` is required.\n- Employees with vacation_rule = none or semesterersattning are excluded: they have no semesterlöneskuld liability.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"payroll:read","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/reports/sie-export":{"get":{"operationId":"reports.sie-export","summary":"SIE4 export (.se file) for a fiscal period.","description":"Returns the period's SIE4 export as text/plain UTF-8. Includes #FNAMN / #ORGNR header, #KONTO chart, #IB/#UB opening + closing balances, #RES result-account totals, and every #VER + #TRANS verifikation in the period. The byte stream matches what the dashboard's `/api/reports/sie-export` produces.\n\n**Use when:** Year-end accountant handoff, migration to another bookkeeping system, audit archival, BFL 7 kap räkenskapsinformation backup.\n**Do not use for:** JSON drilldown of period entries (use /reports/journal-register). Full archive including documents (use /reports/full-archive: not yet on v1).\n\n**Pitfalls:**\n- `period_id` is required.\n- The response is text/plain with Content-Disposition: attachment: clients should treat as a binary download. Filename uses the pattern `export_{period_id}.se`.\n- Default encoding is UTF-8 (no #FORMAT PC8 tag). Pass `encoding=cp437` to get a spec-compliant CP437-encoded file with #FORMAT PC8, required by some legacy desktop bookkeeping software.\n- Only `posted` entries are exported; drafts and reversed entries' originals are included but marked accordingly.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"reports:read","responses":{"200":{"description":"Success","content":{"text/plain":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/imports/sie":{"post":{"operationId":"imports.sie","summary":"Import a SIE4 file.","description":"Accepts a SIE4 file (CP437 / Windows-1252 / UTF-8 auto-detected, up to 50 MB) as the request body, parses it, checks for duplicate imports by file-hash, and replays every #VER + #TRANS into the company's bookkeeping. Returns an `operation_id` immediately: poll `GET /api/v1/operations/{id}` for status + final result. The byte-equivalent dashboard route at /api/import/sie/execute backs the same lib helper, so a SIE imported via v1 matches what the dashboard would produce.\n\n**Use when:** Migrating bookkeeping data from another system (Fortnox, Bokio, Visma) into Accounted, restoring from a backup .se file, or recreating a period from an archive.\n**Do not use for:** Bank transaction CSV/XML imports (use POST /imports/bank). Single-voucher creation (use POST /journal-entries). Importing into a period that already has posted entries: SIE imports run on a fresh period.\n\n**Pitfalls:**\n- Body content-type must be multipart/form-data with a `file` field carrying the .se / .sie file (or a JSON body with `file_base64` for agents that can't do multipart).\n- File size cap: 50 MB. Larger files require chunking client-side or a future streaming import endpoint.\n- Duplicate-file detection is by SHA-256 hash: re-importing the same file returns 409 SIE_IMPORT_DUPLICATE without re-running the import.\n- The operation can take 1-5 minutes for multi-year files. The HTTP response returns immediately with operation_id; poll /operations/{id} every ~2s for status.\n- BFL 7 kap räkenskapsinformation: once a SIE import completes, the resulting verifikationer are immutable. Cancellation midway is not supported.\n- Account mappings are generated server-side from the file's #KONTO records (plus stored per-company overrides). By default the file's account names are carried into the chart, renaming existing accounts whose names differ: pass options.updateAccountNames=false to keep BAS default names.","x-action-risk":"high","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"bookkeeping:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"operation_id":{"type":"string"},"type":{"const":"import.sie"},"status":{"const":"queued"},"poll_url":{"type":"string"}},"required":["operation_id","type","status","poll_url"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/imports/bank":{"post":{"operationId":"imports.bank","summary":"Import a bank-file (CSV / XML / CAMT053).","description":"Accepts a bank statement file (UTF-8 / Windows-1252, up to 10 MB) as multipart/form-data. Auto-detects the bank format (SEB, Swedbank, Handelsbanken, Nordea, Nordea Business, Lansforsakringar, Lunar, ICA Banken, Skandia, CAMT053, generic CSV) or honors a `format` override. Parses transactions, ingests them into the `transactions` table (NOT into journal entries: see BFL note in pitfalls), and emits `transaction.synced` events. Returns operation_id for polling.\n\n**Use when:** Importing a bank statement export for a period. Common with PSD2 bank connections that don't auto-sync, or for legacy bank accounts.\n**Do not use for:** SIE bookkeeping import (use /imports/sie). Auto-bank sync (use the enable-banking extension). Single-transaction creation (use POST /transactions/ingest with a 1-element array).\n\n**Pitfalls:**\n- File size cap: 10 MB. Larger files require splitting client-side.\n- `format` query parameter is optional; auto-detection works for all supported banks. Pass `format` only to force a specific format. Accepted values: seb, swedbank, handelsbanken, nordea, nordea_business, lansforsakringar, ica_banken, skandia, lunar, northmill, generic_csv, camt053.\n- Duplicate detection is by external_id (composed from date + amount + counterparty); a re-import of the same file with the same flag set typically deduplicates rather than creating doubles.\n- BFL 5 kap 6-7 §§ note: this endpoint creates `transactions` rows (the underlag for a verifikation), NOT verifikationer themselves. The verifikation content requirements are in BFL 5 kap 6-7 §§; until each transaction is matched to an invoice/supplier-invoice (POST /transactions/{id}/match-*) or categorised (POST /transactions/{id}/categorize), the bookkeeping obligation isn't discharged. A successful import here means the data is ingested: not booked.\n- A successful import returns operation_id; poll /operations/{id} for the final ingested/duplicates/errors counts.","x-action-risk":"medium","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"transactions:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"operation_id":{"type":"string"},"type":{"const":"import.bank"},"status":{"const":"queued"},"poll_url":{"type":"string"}},"required":["operation_id","type","status","poll_url"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/webhooks":{"get":{"operationId":"webhooks.list","summary":"List webhook subscriptions for a company.","description":"Returns all webhook subscriptions for the company. The HMAC signing secret is never exposed by this endpoint: it is returned exactly once when the webhook is created.\n\n**Use when:** You need to enumerate the webhook subscriptions an integration has registered, e.g. to build a UI listing or sync state with an external system.\n**Do not use for:** Reading delivery history (use GET /webhooks/{id}/deliveries). Reading the secret (it is unrecoverable after the create response: generate a new webhook if lost).\n\n**Pitfalls:**\n- Disabled webhooks (auto-disabled after HTTP 410, or manually disabled via PATCH) appear in the list with active=false and a disabled_reason.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"webhooks:manage","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"webhooks":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"event_type":{"type":"string"},"webhook_url":{"type":"string"},"active":{"type":"boolean"},"api_version_pinned":{"type":"string"},"disabled_at":{"type":"string"},"disabled_reason":{"type":"string"},"created_at":{"type":"string"}},"required":["id","name","event_type","webhook_url","active","api_version_pinned","disabled_at","disabled_reason","created_at"],"additionalProperties":false}}},"required":["webhooks"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}},"post":{"operationId":"webhooks.create","summary":"Register a webhook subscription.","description":"Creates a webhook subscription for one event type. The response includes a freshly generated HMAC signing secret, returned EXACTLY ONCE: store it on the receiver side immediately. The webhook is pinned to the current API version on creation; payload shapes for this webhook will not change until you explicitly upgrade.\n\n**Use when:** You are wiring a downstream integration that needs push notifications instead of polling.\n**Do not use for:** Subscribing to internal MCP telemetry events (mcp.tool_called etc. are not delivered as webhooks). Replacing an existing webhook URL: use PATCH instead.\n\n**Pitfalls:**\n- The secret is returned exactly once. If lost, delete and recreate the webhook.\n- Delivery is at-least-once with exponential backoff (1m / 5m / 30m / 2h / 12h / 24h / 48h). Receivers MUST be idempotent.\n- HTTP 410 from your receiver auto-disables the webhook (sets active=false + disabled_reason).","x-action-risk":"low","x-idempotent":true,"x-reversible":true,"x-dry-run-supported":true,"x-required-scope":"webhooks:manage","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"event_type":{"type":"string"},"webhook_url":{"type":"string"},"active":{"type":"boolean"},"api_version_pinned":{"type":"string"},"disabled_at":{"type":"string"},"disabled_reason":{"type":"string"},"created_at":{"type":"string"},"secret":{"type":"string"},"description":{"type":"string"}},"required":["id","name","event_type","webhook_url","active","api_version_pinned","disabled_at","disabled_reason","created_at","secret","description"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/webhooks/{id}":{"get":{"operationId":"webhooks.get","summary":"Get a webhook subscription by id.","description":"Returns the webhook configuration. The HMAC signing secret is never exposed.\n\n**Use when:** You need the current state of a single webhook (e.g. to render a settings page).\n**Do not use for:** Reading the secret (returned only once on creation).","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"webhooks:manage","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"event_type":{"type":"string"},"webhook_url":{"type":"string"},"active":{"type":"boolean"},"api_version_pinned":{"type":"string"},"disabled_at":{"type":"string"},"disabled_reason":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","name","description","event_type","webhook_url","active","api_version_pinned","disabled_at","disabled_reason","created_at","updated_at"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}},"patch":{"operationId":"webhooks.update","summary":"Update a webhook subscription.","description":"Update the URL, name, description, or active flag. event_type is immutable: delete and recreate to change it. Setting active=false manually pauses delivery without deleting; setting active=true clears any disabled_at/disabled_reason set by the auto-disable on HTTP 410.\n\n**Use when:** You need to point an existing webhook at a new URL or temporarily pause delivery.\n**Do not use for:** Rotating the signing secret (delete and recreate). Changing event_type.\n\n**Pitfalls:**\n- Re-enabling a webhook (active: true) does NOT replay deliveries that went to dead status while it was disabled: those need POST /webhook-deliveries/{id}/retry.","x-action-risk":"low","x-idempotent":true,"x-reversible":true,"x-dry-run-supported":true,"x-required-scope":"webhooks:manage","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"event_type":{"type":"string"},"webhook_url":{"type":"string"},"active":{"type":"boolean"},"api_version_pinned":{"type":"string"},"disabled_at":{"type":"string"},"disabled_reason":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","name","description","event_type","webhook_url","active","api_version_pinned","disabled_at","disabled_reason","created_at","updated_at"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}},"delete":{"operationId":"webhooks.delete","summary":"Delete a webhook subscription.","description":"Hard-deletes the webhook. The delivery audit trail SURVIVES: both terminal (delivered, dead) and non-terminal (pending, failed) delivery rows persist with webhook_id = NULL so the BFNAR 2013:2 kap 8 § behandlingshistorik (7-year retention) for accounting-event deliveries is preserved. Non-terminal rows go dormant (the dispatcher skips them).\n\n**Use when:** You no longer want this webhook to receive events.\n**Do not use for:** Temporarily pausing delivery: use PATCH with active=false instead so the configuration survives.\n\n**Pitfalls:**\n- Audit history survives DELETE; only the receiver subscription is removed. To suppress future events without retaining the registration use PATCH active=false.","x-action-risk":"medium","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"webhooks:manage","responses":{"204":{"description":"No Content"},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/webhooks/{id}/test":{"post":{"operationId":"webhooks.test","summary":"Send a synthetic test event to a webhook.","description":"Enqueues a webhook.test delivery against the configured receiver. The dispatcher delivers it on the next per-minute cron tick. Use the returned webhook_delivery_id to poll GET /webhooks/{id}/deliveries for the outcome.\n\n**Use when:** After creating or modifying a webhook, before relying on it in production: to validate that the receiver is reachable and that signature verification works on the receiver side.\n**Do not use for:** Smoke-testing the dispatcher itself (use a real event). Replaying a failed delivery (use POST /webhook-deliveries/{id}/retry).\n\n**Pitfalls:**\n- Test deliveries follow the same retry policy as real events: a 500 from your receiver will retry 7 times over ~87h (about 3.6 days). Use a 2xx ack-only handler if you want a clean signal.","x-action-risk":"low","x-idempotent":false,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"webhooks:manage","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"webhook_delivery_id":{"type":"string"},"status":{"const":"pending"}},"required":["webhook_delivery_id","status"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/webhooks/{id}/deliveries":{"get":{"operationId":"webhooks.deliveries.list","summary":"List deliveries for a webhook subscription.","description":"Returns deliveries for the webhook in newest-first order. Each row carries the current status (pending / in_flight / delivered / failed / dead), the attempt count, the next scheduled retry time, and the captured response details from the last attempt.\n\n**Use when:** You are debugging a flaky receiver, or building a delivery-history UI for a settings page.\n**Do not use for:** Listing deliveries across multiple webhooks (this endpoint is single-webhook scoped).\n\n**Pitfalls:**\n- response_body is truncated to 4 KB: receivers returning long error pages have their response truncated.\n- A delivery in `failed` status is non-terminal: the dispatcher will retry it at next_attempt_at. `dead` is terminal.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"webhooks:manage","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"webhook_id":{"type":"string"},"event_type":{"type":"string"},"status":{"type":"string","enum":["pending","in_flight","delivered","failed","dead"]},"attempts":{"type":"number"},"next_attempt_at":{"type":"string"},"response_status":{"type":"number"},"response_body":{"type":"string"},"error":{"type":"string"},"request_id":{"type":"string"},"created_at":{"type":"string"},"delivered_at":{"type":"string"}},"required":["id","webhook_id","event_type","status","attempts","next_attempt_at","response_status","response_body","error","request_id","created_at","delivered_at"],"additionalProperties":false}},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/webhook-deliveries/{id}/retry":{"post":{"operationId":"webhook_deliveries.retry","summary":"Retry a webhook delivery.","description":"Re-enqueues a dead (or delivered) delivery as a fresh pending row. The new delivery references the same webhook + payload; the dispatcher picks it up at the next per-minute cron tick. The original row is preserved in the audit log.\n\n**Use when:** After a receiver outage you want to replay deliveries that died, or after fixing a receiver-side bug you want to redeliver a successful one.\n**Do not use for:** Retrying live deliveries (pending / in_flight / failed): the dispatcher is already managing them.\n\n**Pitfalls:**\n- Retrying a delivered delivery causes the receiver to see the event twice. Receivers MUST be idempotent (check the X-Gnubok-Delivery header).","x-action-risk":"medium","x-idempotent":false,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"webhooks:manage","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"webhook_delivery_id":{"type":"string"},"status":{"const":"pending"}},"required":["webhook_delivery_id","status"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/webhooks/{id}/rotate-secret":{"post":{"operationId":"webhooks.rotate_secret","summary":"Rotate the HMAC signing secret on a webhook.","description":"Generates a fresh HMAC signing secret for the webhook and returns it EXACTLY ONCE. The previous secret is invalidated immediately. There is no grace period: coordinate the rotation on the receiver side BEFORE calling this endpoint, or temporarily disable the webhook (PATCH active=false) to pause delivery while you swap secrets.\n\n**Use when:** After a suspected secret leak, on a routine rotation cadence (Stripe pattern: every 90 days for compliance-grade integrations), or when changing the receiver implementation and you want to invalidate the old secret deliberately.\n**Do not use for:** Routine integration setup: the secret returned at create time is the canonical one. Recovering a lost secret (rotation does not recover the prior value; it issues a fresh one).\n\n**Pitfalls:**\n- The secret is returned exactly once. If you lose this response, the recovery path is to rotate again.\n- In-flight deliveries between the rotation and the receiver-side update may fail signature verification on the new secret. Pause the webhook (PATCH active=false) first if your tolerance for that window is zero.","x-action-risk":"medium","x-idempotent":false,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"webhooks:manage","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"secret":{"type":"string"},"rotated_at":{"type":"string"}},"required":["id","secret","rotated_at"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/inbox-items/{id}/stamp":{"post":{"operationId":"inbox-items.stamp","summary":"Mark an inbox item as consumed by a journal entry.","description":"Sets created_journal_entry_id on an invoice_inbox_items row so the item drops out of the active inbox todo list. Use when the document was linked to a JE via a separate call and you need to close the inbox item independently.\n\n**Use when:** An inbox document has already been attached to a verifikation (via documents link) but the inbox item itself was not stamped at link time: e.g. when using the v1 link endpoint without inbox_item_id.\n**Do not use for:** Creating a new journal entry from an inbox item: use the invoice-inbox extension book-direct route for that.\n\n**Pitfalls:**\n- Idempotency-Key is mandatory.\n- The inbox item and journal_entry_id must both belong to the caller's company.\n- Stamping with a different journal_entry_id than the one already set returns CONFLICT: the item is already resolved.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"documents:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"created_journal_entry_id":{"type":"string"}},"required":["id","created_journal_entry_id"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/dimensions":{"get":{"operationId":"dimensions.list","summary":"List dimensions (kostnadsställe/projekt) with their values.","description":"Returns the company's dimension registry: SIE #DIM entries keyed by sie_dim_no (1 = Kostnadsställe, 6 = Projekt; both always exist): with the registered values (#OBJEKT) nested under each dimension. Dimensions are ordered by sort_order, values by code. Line-level tags on journal entries reference these values as {\"<sie_dim_no>\":\"<code>\"} in the `dimensions` map.\n\n**Use when:** You need the valid dimension value codes before tagging journal-entry lines with a cost centre or project, or you are rendering a dimension picker.\n**Do not use for:** Filtering reports (pass the dimension filter to the report endpoints once available) or reading which lines carry a tag (read the journal entries themselves).\n\n**Pitfalls:**\n- Dimension value codes are STRINGS and case-sensitive: \"P001\", not 1.\n- sie_dim_no is the key used in journal_entry_lines.dimensions, NOT the dimension row id.\n- is_active=false values are historical (archived): do not tag new lines with them.\n- resets_annually=true (dim 1) means balances reset each fiscal year; dim 6 (projekt) accumulates across years.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"reports:read","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"dimensions":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"sie_dim_no":{"type":"number"},"name":{"type":"string"},"resets_annually":{"type":"boolean"},"is_system":{"type":"boolean"},"is_active":{"type":"boolean"},"sort_order":{"type":"number"},"values":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"code":{"type":"string"},"name":{"type":"string"},"is_active":{"type":"boolean"},"start_date":{"type":"string"},"end_date":{"type":"string"}},"required":["id","code","name","is_active","start_date","end_date"],"additionalProperties":false}}},"required":["id","sie_dim_no","name","resets_annually","is_system","is_active","sort_order","values"],"additionalProperties":false}}},"required":["dimensions"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/dimensions/{id}/values":{"post":{"operationId":"dimensions.values.create","summary":"Create a dimension value (kostnadsställe/projekt code).","description":"Registers a new value (SIE #OBJEKT) under a dimension: e.g. a new project code under dimension 6. Requires Idempotency-Key (UUID). Supports ?dry_run=true to validate the code format without committing. The `:id` path segment is the dimension row id (from GET …/dimensions), not the sie_dim_no. Duplicate codes within the dimension return 409 DIMENSION_VALUE_DUPLICATE_CODE.\n\n**Use when:** A voucher or invoice references a cost centre / project code that does not exist yet and the user has confirmed it should be created.\n**Do not use for:** Renaming or archiving an existing value (dashboard register in v1). Tagging lines: pass the dimensions map on the journal-entry line instead.\n\n**Pitfalls:**\n- Idempotency-Key is mandatory: calls without it return 400 VALIDATION_ERROR.\n- The :id segment is the dimension UUID, not the SIE dimension number.\n- Codes are limited to the strict Fortnox charset (A-Ö, digits, _, +, -; max 20 chars) even though historical imported codes may be looser.\n- code is immutable after creation: there is no rename in v1; create the correct code and archive the wrong one.","x-action-risk":"low","x-idempotent":true,"x-reversible":true,"x-dry-run-supported":true,"x-required-scope":"bookkeeping:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"dimension_id":{"type":"string"},"code":{"type":"string"},"name":{"type":"string"},"is_active":{"type":"boolean"},"start_date":{"type":"string"},"end_date":{"type":"string"},"created_at":{"type":"string"}},"required":["id","dimension_id","code","name","is_active","start_date","end_date","created_at"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/dimensions/{id}/values/{valueId}":{"patch":{"operationId":"dimensions.values.update","summary":"Update a dimension value (rename, archive, set start/end date).","description":"Sparse update of a dimension value (SIE #OBJEKT): name, is_active (false = archive), start_date, end_date. `code` is immutable: renaming a code would orphan every journal line tagged with it; create a new value and archive the old one instead. Dates are only allowed on accumulating dimensions (resets_annually=false, e.g. dim 6 Projekt): use end_date to close a finished project. Idempotent (mandatory Idempotency-Key) and dry-runnable.\n\n**Use when:** You need to rename a project/cost-centre, mark a finished project with an end date, or archive (is_active=false) a value that should no longer be used on new lines.\n**Do not use for:** Changing the code (immutable: create + archive instead). Removing an unused value entirely (use DELETE). Tagging lines (pass dimensions on the journal-entry line or invoice).\n\n**Pitfalls:**\n- Idempotency-Key is mandatory.\n- The :id segment is the dimension UUID and :valueId the value UUID (both from GET …/dimensions), not SIE numbers or codes.\n- start_date/end_date return 400 DIMENSION_VALUE_DATES_NOT_ALLOWED on resets_annually dimensions (dim 1 Kostnadsställe).\n- Archived values (is_active=false) still appear in GET …/dimensions and remain valid on historical lines; they are only blocked for NEW tags.","x-action-risk":"low","x-idempotent":true,"x-reversible":true,"x-dry-run-supported":true,"x-required-scope":"bookkeeping:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"dimension_id":{"type":"string"},"code":{"type":"string"},"name":{"type":"string"},"is_active":{"type":"boolean"},"start_date":{"type":"string"},"end_date":{"type":"string"}},"required":["id","dimension_id","code","name","is_active","start_date","end_date"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}},"delete":{"operationId":"dimensions.values.delete","summary":"Delete an unreferenced dimension value.","description":"Hard-deletes a dimension value (SIE #OBJEKT) that no journal line references. Values used on posted or reversed verifikat are retained for the BFL 7-year archive and cannot be deleted: the DB trigger blocks it and this endpoint returns 409 DIMENSION_VALUE_REFERENCED. Archive those instead (PATCH is_active=false). Requires Idempotency-Key.\n\n**Use when:** A project/cost-centre code was created by mistake (typo, duplicate) and has never been used on any booking.\n**Do not use for:** Retiring a project that has bookings: PATCH is_active=false (and optionally end_date) instead. Deleting a whole dimension (not supported).\n\n**Pitfalls:**\n- Idempotency-Key is mandatory.\n- 409 DIMENSION_VALUE_REFERENCED means the value is used on booked verifikat: it can never be deleted, only archived.\n- Deletion is permanent: the code can be re-created afterwards, but the old row id is gone.","x-action-risk":"medium","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"bookkeeping:write","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"deleted":{"const":true},"id":{"type":"string"}},"required":["deleted","id"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}},"/api/v1/companies/{companyId}/articles":{"get":{"operationId":"articles.list","summary":"List the article register (artikelregister).","description":"Returns the company's articles ordered by name. Pass ?include_inactive=true to include soft-deactivated articles. Use the returned id as items[].article_id when creating invoices; housework_type carries the ROT/RUT arbetstypskod for service articles, and revenue_account the optional BAS class-3 override.\n\n**Use when:** You need the article catalog before composing invoice lines: to resolve an article_id, read its price/VAT defaults, or find ROT/RUT-tagged service articles (housework_type set).\n**Do not use for:** Creating or editing articles (dashboard-only for now). Invoice line creation itself (POST …/invoices with items[].article_id).\n\n**Pitfalls:**\n- Linking article_id does NOT auto-fill the invoice line: send description, unit_price, vat_rate etc. explicitly on the item (copy them from this response).\n- price_excl_vat always excludes VAT.\n- housework_type is an arbetstypskod hint (e.g. BYGG, STAD); the invoice line still needs deduction_type + labor_hours + work_type set explicitly for ROT/RUT.\n- Inactive articles (active=false) are hidden by default but remain linkable for historical reads.","x-action-risk":"low","x-idempotent":true,"x-reversible":false,"x-dry-run-supported":false,"x-required-scope":"invoices:read","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"articles":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"article_number":{"type":"string"},"name":{"type":"string"},"name_en":{"type":"string"},"type":{"type":"string","enum":["vara","tjanst"]},"unit":{"type":"string"},"price_excl_vat":{"type":"number"},"vat_rate":{"type":"number"},"revenue_account":{"type":"string"},"cost_price":{"type":"number"},"ean":{"type":"string"},"housework_type":{"type":"string"},"notes":{"type":"string"},"active":{"type":"boolean"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","article_number","name","name_en","type","unit","price_excl_vat","vat_rate","revenue_account","cost_price","ean","housework_type","notes","active","created_at","updated_at"],"additionalProperties":false}}},"required":["articles"],"additionalProperties":false},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"api_version":{"type":"string"},"next_cursor":{"type":"string"},"audit":{"type":"object","properties":{"voucher_number":{"type":"string"},"voucher_url":{"type":"string"},"audit_trail_url":{"type":"string"},"immutable_at":{"type":"string"}},"additionalProperties":false},"partial_expansions":{"type":"array","items":{"type":"string"}}},"required":["request_id","api_version"],"additionalProperties":false}},"required":["data","meta"],"additionalProperties":false}}}},"400":{"description":"Validation error","$ref":"#/components/responses/Error"},"401":{"description":"Unauthorized","$ref":"#/components/responses/Error"},"403":{"description":"Insufficient scope","$ref":"#/components/responses/Error"},"404":{"description":"Not found","$ref":"#/components/responses/Error"},"429":{"description":"Rate limited","$ref":"#/components/responses/Error"},"500":{"description":"Internal error","$ref":"#/components/responses/Error"}}}}}}