# Norsk GL — REST API > AI-native Norwegian general ledger (hovedbok). Legal accounting invariants > (double-entry balance, append-only, period locking, voucher numbering) are > enforced INSIDE PostgreSQL — the API is a thin shell. Field names and error > messages are Norwegian by design. Current contract: API version 2026-09-03.075, requiring database schema migration 075. The public connector remains a read-only migration-062 contract. Bris Mobil uses the migration-071 app-db/v3 contract: it may register only a hash-bound approval; payment, MVA submission and generic event routes remain unavailable. ## Platform: build your own accounting system Norsk GL is a B2D platform ("Stripe for the Norwegian general ledger"): you build the frontend and product experience, Norsk GL owns the ledger and the regulated plumbing. This file is written so an AI coding agent (Claude, Codex, Lovable or similar) can build a complete Norwegian accounting product against the API without further documentation. What you get through one API key: - A legally correct Norwegian ledger: double-entry enforced in PostgreSQL, the full NS 4102 standard chart of accounts (745 SAF-T accounts), all Skatteetaten MVA codes, versioned corrections, soft period locks, and hash-chained audit trails per client. - SAF-T Financial export validated against Skatteetaten's XSD — your product is SAF-T ready on day one (GET /klienter/{id}/saf-t). - Invoicing with PDF and Peppol BIS 3.0 (EHF) document foundations, personal ledgers for housing co-ops, bank-transaction storage and reconciliation. - MVA return aggregation per term with XSD-validated XML (submission to the authorities is a coming gateway; see below). Tenancy: you are a **partner**; your customers are **klienter** (each a legal entity with its own ledger). Row-level security isolates everything. API keys are partner-wide or locked to one klient, and can carry gateway scopes. Access is commercial per partner; contact the operator to be onboarded (self-service signup is planned but deliberately not open yet). ## Gateways (shared integration modules) The gateway foundation is live: every module is activated per partner (agreement) and per klient (activation), gated by globally controlled ports, with hash-chained usage evidence for billing. Levels: les (read), skriv (write, no external effect), utfor (external effect: documents leave the platform, money moves, filings are submitted). All utfor ports are BORN CLOSED and require an explicit, logged owner decision to open. | Gateway | Purpose | Status | |--------------|----------------------------------------------------|--------| | peppol | Send/receive EHF invoices with delivery evidence | document foundation live, transport coming | | bank | Account transactions, CAMT, reconciliation (AISP) | storage/matching live, feed coming | | betaling | Payment initiation (PISP) — the only exit for money| planned | | innbetaling | KID/OCR, eFaktura, AvtaleGiro | planned | | bilagsmottak | Inbound EHF/e-mail/PDF to voucher drafts (AI/OCR) | planned | | rapportering | MVA and other filings via Altinn 3/Maskinporten | XML live, submission coming | | kort_utlegg | Card transactions and expense approval | planned | | revisor | Auditor packages, trial balance, audit trail (read)| ledger data live via REST | | saft | SAF-T Financial export/import | LIVE | ## Quickstart for an AI agent 1. GET /versjon and /llms.txt (this file) to confirm the contract. 2. POST /klienter with orgnr + navn (mal_type "full" gives the complete NS 4102 chart) — the ledger is immediately ready for postings. 3. POST a balanced voucher: lines with konto, beloep_oere (integer øre, always positive) and dc_sign (1 debit / -1 credit); set mva_kode on the base line. The database assigns bilagsnummer; display bilagsref (e.g. "1-2026"). 4. Read saldo/resultat/balanse endpoints for reporting; GET .../saf-t for the audit file. Corrections are always NEW postings (append-only) — build your UI accordingly (no edit-in-place of posted vouchers). 5. Handle the error model below verbatim: BOKF errors are Norwegian bookkeeping law saying no — show them to the user, do not retry. ## Authentication All endpoints except /helse, /versjon and /llms.txt require: Authorization: Bearer Clients may send x-correlation-id (UUID). The same value is returned in the response header, propagated through partner calls and attached to new audit events. Errors also return korrelasjons_id. GET /versjon exposes the API and latest recorded schema-migration version. ## Core concepts - **Amounts are integer øre** (1 NOK = 100 øre). Field: beloep_oere. - **Direction**: dc_sign = 1 (debit) / -1 (credit). Amounts are always positive. - **Append-only**: postings are NEVER updated or deleted. Corrections are new postings referencing the original via reverserer_id. - **Periods**: monthly (aar + maaned). A posting's date must fall inside an OPEN period for that client. Locking is a logged soft lock; controlled reopening requires an actor/reason and preserves the complete history. - **Voucher numbers** (bilagsnummer) are assigned by the database, sequentially per client. You cannot set them. The STANDARD human reference is bilagsref = "NUMBER-YEAR" (e.g. 1-2026), year from the posting date — computed by the SQL function bilagsref(nummer, dato) and returned as a bilagsref field wherever a voucher number is exposed. Always display this form. - **MVA codes** follow Skatteetaten's SAF-T standard list (1, 3, 5, 11, ...). Set mva_kode on the BASE line (revenue/cost), not on the VAT account line. ## Error model Errors are JSON: { "feiltype": "...", "melding": "...", "korrelasjons_id": "uuid" } - 422 feiltype=BOKF — Norwegian bookkeeping law said no (e.g. unbalanced posting, locked period, append-only violation). Message starts with "BOKF:". - 400/404 feiltype=GL — technical error ("GL: Ukjent klient ..." -> 404). - 409 feiltype=KONFLIKT — duplicate (e.g. orgnr already exists). - 422 feiltype=VALIDERING — constraint violation (e.g. invalid orgnr mod11). ## Endpoints ### Health GET /helse -> { status, database, api_versjon } GET /versjon -> { tjeneste, api_versjon, feilmodell, siste_migrasjon, korrelasjons_id } ### Clients POST /klienter { orgnr, navn, kontakt_fornavn?, kontakt_etternavn?, mal_type?: "full"|"forenklet"|"ns4102"|"boligselskap" } Creates the client AND atomically copies the chosen chart. "full" is the 567-account default; "boligselskap" is the focused 23-account chart for borettslag/sameier. orgnr is mod11-validated. GET /klienter GET /klienter/:klientId PATCH /klienter/:klientId { navn?, adresse?, postnr?, poststed?, bankkontonr?, mva_registrert?, foretaksregisteret?, organisasjonsform?:"ENK"|"AS"|"ASA"|"BRL"|"ESEK", hovedkontor_adresse?, hovedkontor_postnr?, hovedkontor_poststed?, hovedkontor_land?, under_avvikling?, mva_registrert_ved_representant?, mva_representant_navn?, mva_representant_adresse?, mva_representant_postnr?, mva_representant_poststed?, standard_betalingsfrist_dager? } Legal seller master data used on sales invoices. Issue requires an explicitly verified supported organisation form plus explicit Foretaksregisteret, liquidation and VAT-representative status. AS/ASA require Foretaksregisteret and a complete structured head-office address; BRL requires Foretaksregisteret. Representative details are required when the representative flag is true; never infer legal status from chat text. GET /klienter/:klientId/kontoplan -> all accounts (kontonummer, navn, saft_kode, gruppering_kategori/kode, aktiv) ### Bris action queue (server-to-server; never call from a public connector) The public ChatGPT/Claude connector only receives a minimized summary. Bris Mobil re-authenticates the user, resolves tenant membership itself, and then uses these Bris-partner-authenticated endpoints. Sensitive details, task IDs and approval hashes must never be put in a chat URL. This queue is partner-isolated to the Bris workflow. Active OAuth membership is the connector access decision; there is no connector routing. Independent partner-API consumers do not share this queue or its identities. GET /klienter/:klientId/handlingsoppgaver?antall=25 -> active summary rows for the authenticated tenant. POST /klienter/:klientId/handlingsoppgaver Requires x-actor-id and a write-capable partner key. Creates an immutable, idempotent Bris package after server-side partner/client authorization. POST /klienter/:klientId/handlingsoppgaver/:handlingspakkeId/forhandsvisning Requires a write-capable server key. Returns full details only after the Bris app has authenticated/authorized the end user. POST /klienter/:klientId/handlingsoppgaver/:handlingspakkeId/hendelser Requires x-actor-id. Approval/execution events carry forventet_innhold_hash; invalid transitions, expired packages and changed retries are rejected. ### Periods POST /klienter/:klientId/perioder { aar, maaned } -> 201 GET /klienter/:klientId/perioder POST /klienter/:klientId/perioder/:aar/:maaned/laas -> logged soft lock POST /klienter/:klientId/perioder/:aar/:maaned/aapne { aktoer?, aarsak? } -> controlled reopen ### Bookkeeping POST /klienter/:klientId/bilag Body: { beskrivelse: string, posteringer: [{ posteringsdato: "YYYY-MM-DD", // must be in an open period beskrivelse?: string, reverserer_id?: uuid, // for corrections linjer: [{ kontonummer: "NNNN", beloep_oere: int > 0, dc_sign: 1 | -1, mva_kode?: string, beskrivelse?: string }] }] } Atomic: the whole voucher commits or nothing does. Each posting must balance to the øre (sum of beloep_oere * dc_sign = 0) and have >= 2 lines. -> 201 { bilag_id, bilagsnummer, posteringer: [uuid] } ### Corrections & locks (versioning model) Postings are versioned, not overwritten — a correction supersedes the current line set and inserts a new one; nothing is ever deleted. Corrections are gated by three lock levels. POST /klienter/:klientId/posteringer/:posteringId/korriger Body: { endret_av?, linjer: [ ...same shape as bilag lines... ] } Replaces the posting's current line set with a new (balanced, >=2 line) set. Rejected (422 BOKF) if: the period is locked (reopen first), the year is closed (reopen the year), or the change affects VAT in a termin whose VAT return is filed (reverse the return first). A konto change that does NOT change the VAT footprint is allowed even after the return is filed. POST /klienter/:klientId/perioder/:aar/:maaned/aapne { aktoer?, aarsak? } Reopens a locked period (soft lock). Logged immutably. Re-lock with .../laas. POST /klienter/:klientId/mva-melding/:aar/:termin/lever { aktoer? } Marks the VAT return for the termin (1-6) as filed; stores the generated XML immutably and locks VAT-affecting changes in that termin. POST /klienter/:klientId/mva-melding/:aar/:termin/reverser { aktoer? } Reverses a filed VAT return so VAT-affecting corrections are allowed again. POST /klienter/:klientId/aar/:aar/avslutt { aktoer?, aarsak? } Closes the year (tax return filed) — freezes ALL changes for that year. POST /klienter/:klientId/aar/:aar/gjenaapne { aktoer?, aarsak? } Reopens a closed year so corrections are possible again. ### Drafts (AI-native inflow) — the primary agent workflow A draft (bilagsutkast) is a MUTABLE proposal living OUTSIDE the append-only ledger. An agent proposes a voucher from a source document; the draft can be edited or discarded freely until it is booked. bokfor is the ONLY path into the ledger and reuses every invariant. POST /klienter/:klientId/utkast Body: { beskrivelse, posteringsdato:"YYYY-MM-DD", opphav?:"ai_forslag"|"manuell"|"import", kilde_dokument_ref?, konfidens?:0..1, foreslatt_av?, linjer?:[ ...bilag line shape... ] } -> 201 the draft (status "utkast") GET /klienter/:klientId/utkast?status=utkast|bokfoert|forkastet GET /klienter/:klientId/utkast/:utkastId -> draft + linjer[] PUT /klienter/:klientId/utkast/:utkastId { beskrivelse?, posteringsdato?, konfidens?, linjer? } Edits a draft (only while status="utkast"; linjer replaces the whole set). GET /klienter/:klientId/utkast/:utkastId/valider -> { klar: boolean, problemer: string[] } (empty = ready to book). Call this before bokfor to get balance/account/VAT/period feedback. POST /klienter/:klientId/utkast/:utkastId/bokfor { godkjent_av? } -> 201 { postering_id } Books the draft into the append-only ledger, stamping provenance (opphav, kilde_dokument_ref, konfidens, godkjent_av) on the posting. POST /klienter/:klientId/utkast/:utkastId/forkast -> discards the draft Typical AI flow: read invoice -> POST /utkast (with konfidens) -> GET .../valider -> fix via PUT if needed -> POST .../bokfor (human or high-confidence agent approves). POST /klienter/:klientId/utkast/leverandorkreditnota { idempotensnoekkel, original_postering_id, beskrivelse, posteringsdato, kreditnotanr, motkonto, brutto_oere, mva_kode?, dimensjoner? } -> an approvable draft with opposite expense/VAT/payable lines and a permanent link to the original supplier invoice. Full and partial credits are supported; over-crediting and incompatible VAT treatment are rejected. GET /klienter/:klientId/leverandorfakturaer -> supplier invoices still available for credit, including paid invoices (their later credit remains open until the supplier refund is received). ### Posting suggestions (konteringsforslag) — A3 Given free text (and optional counterparty), suggest which account + VAT code to use, ranked by confidence. Sources, best first: explicit rules (konteringsregel), the client's OWN posting history (fuzzy text match via pg_trgm), and the account's standard_mva_kode as fallback. Suggestions only — feed the chosen line into a draft (POST /utkast); nothing is booked here. POST /klienter/:klientId/konteringsforslag Body: { tekst: string, motpart?: string, beloep_oere?: int } -> { forslag: [{ kontonummer, konto_navn, mva_kode, konfidens (0..1), kilde: "regel"|"historikk"|"kontonavn", begrunnelse }] } (top 5, best first) ### Posting rules (konteringsregel) Explicit konto(+mva) hints keyed by a text/counterparty nokkel. NOT ledger data — freely editable. A rule without mva_kode inherits the account's standard_mva_kode. GET /klienter/:klientId/konteringsregler POST /klienter/:klientId/konteringsregler { nokkel, kontonummer, mva_kode? } -> 201 { regel_id } DELETE /klienter/:klientId/konteringsregler/:regelId Suggestion-driven AI flow: read invoice -> POST /konteringsforslag (tekst+motpart) -> take the top forslag -> POST /utkast with that konto+mva and a stable idempotensnoekkel -> valider -> bokfor. Retry with the same key. ### Reskontro (subledger) — B2 Accounts 1500 (kunde), 2400 (leverandør), 2910 (ansatt) are reskontro accounts: any line on them MUST carry a motpart_id (BOKF: otherwise), and motpart_id is forbidden on non-reskontro lines. Posting lines (bilag/utkast/korriger) therefore accept extra fields: motpart_id, forfallsdato (YYYY-MM-DD), fakturanr, kid. Income may still be booked straight to bank without a kundefordring. POST /klienter/:klientId/motparter { type:"kunde"|"leverandør"|"ansatt", navn, orgnr? } -> 201 motpart with auto reskontronr (kunde 10000+, leverandør 20000+, ansatt 30000+). GET /klienter/:klientId/motparter?type= GET /klienter/:klientId/aapne-poster?motpart_id=&dato= -> open (unmatched) reskontro items: { posteringslinje_id, motpart_navn, kontonummer, fakturanr, kid, forfallsdato, belop_oere (signed), dager_forfalt }. GET /klienter/:klientId/aldersfordeling?type=&motpart_id=&dato= -> aging per motpart: ikke_forfalt / d1_30 / d31_60 / d61_90 / over_90 / sum_oere (øre). POST /klienter/:klientId/lukkinger { linje_ids:[uuid,uuid,...], aktoer? } -> 201 { lukking_id }. Match open items that net to 0 for one motpart (full settlement). Append-only. POST /klienter/:klientId/lukkinger/:lukkingId/reverser { aktoer? } -> 201 reopens the items (new reversal event; nothing is deleted). POST /klienter/:klientId/banktransaksjoner/:txId/oppgjor { linje_ids:[uuid,...], aktoer } -> atomically books bank+subledger, closes all selected open items and matches the bank evidence. A set may contain an invoice plus partial credit notes; the bank transaction is the idempotency key. PATCH /klienter/:klientId/motparter/:motpartId { navn?, orgnr?, adresse?, postnr?, poststed?, land?, epost?, kontaktperson?, aktiv?, mva_registrert? } -> updated motpart. mva_registrert is an explicitly verified buyer status; a Norwegian buyer with orgnr must have true or false set before issue. ### Sales invoices (faktura) — document layer over the ledger A faktura is a mutable draft until issued; issuing assigns a gap-free number (per client, bokf.forskr. § 5-1-3), books it to the ledger, and locks it. Corrections are made ONLY by credit note (kreditnota), never by editing. Amounts in ØRE; invoice numbers and all invoice amount fields are decimal strings in REST responses so bigint precision is never lost. VAT rounded per line. mva_kode: 3=25% 31=15% 33=12% 5=exempt 52=export 6=outside VAT. Line inntektskonto defaults per mva_kode (3000/3100/3200). POST /klienter/:klientId/fakturaer { motpart_id, leveringsdato?, leveringssted?, forfallsdato?, deres_referanse?, vaar_referanse?, fritekst?, linjer:[{ beskrivelse, antall?, enhet?, enhetspris_oere, rabatt_prosent?, mva_kode, inntektskonto? }] } -> 201 faktura document (draft). GET /klienter/:klientId/fakturaer?status=&type= -> list rows (with brutto_oere). GET /klienter/:klientId/fakturaer/:fakturaId -> full faktura document (selger, kjoper, linjer, mva_spesifikasjon, totaler, bokfoert:{bilag_id,bilagsnummer}). GET /klienter/:klientId/fakturaer/:fakturaId/efaktura-originalstatus -> read-only, tenant-bound status for an issued invoice: "ikke_generert" or "validert_original". A validated result includes only IDs, profile metadata, timestamps and SHA-256 hashes. It never returns XML bytes, validation-report bytes, worker functions or transport controls. sendeklar and ekstern_sending_aktiv are always false in this contract. GET /klienter/:klientId/fakturaer/:fakturaId/pdf -> application/pdf visual copy. Drafts render live and are visibly marked Utkast. Documents backfilled from pre-057 data are visibly marked as historical reconstructions. New issued v2 documents return their byte-exact, append-only archived issuance copy and expose its snapshot/PDF hashes plus archive metadata in response headers. GET never creates accounting evidence as a side effect. It returns 409 if an issued v2 document is missing its archived copy. An archived issuance copy proves neither transmission nor receipt. NEVER put PDF bytes, base64, OCR/extracted PDF content or the PDF response into model, prompt, tool-result or connector context. Fetch/display it only in an authenticated user interface outside model context. Rendering has a bounded, tenant-isolated single-flight/cache queue. A full queue returns HTTP 503 with Retry-After; retry the idempotent GET later. PUT /klienter/:klientId/fakturaer/:fakturaId -> update draft (same body). BOKF if issued. DELETE /klienter/:klientId/fakturaer/:fakturaId -> delete draft. BOKF if issued. POST /klienter/:klientId/fakturaer/:fakturaId/utsted { utstedt_av? } -> issues: assigns fakturanummer, books (netto->3xxx, mva->2700-series, brutto->1500 kundefordring w/motpart), creates an immutable SHA-256-bound document snapshot, locks, and archives the rendered PDF issuance copy. leveringsdato + leveringssted are required before issue. Auto-opens the period if missing. If issue commits but archival is interrupted, use the explicit recovery endpoint below before reading or distributing the PDF. POST /klienter/:klientId/fakturaer/:fakturaId/pdf/arkiver { arkivert_av? } -> idempotently creates or recovers the byte-exact PDF issuance copy for an issued v2 snapshot. This is a write-authorized endpoint; drafts and historical reconstructions are rejected. The database winner is canonical. POST /klienter/:klientId/fakturaer/:fakturaId/kreditnota { aarsak?, opprettet_av? } -> 201 credit note (mirrors + reverses the booking), including snapshot and archived PDF issuance copy; original -> status 'kreditert'. Schema migration 058 creates document version 2 for new issuance. It freezes the legal seller fields (organisation form, register name, structured head office, liquidation state and VAT-representative state/details), buyer VAT status, lines, VAT specification and totals in the SHA-256-bound JSON. Later seller or buyer master-data changes cannot alter an issued document or its hash. Historical version-1 snapshots remain immutable. Schema migration 059 adds the byte-exact, append-only PDF issuance-copy archive with tenant/snapshot binding, SHA-256 verification and minimum-retention metadata. This internal copy is not proof of external transmission. Migrations 060–062 make the credit-note actor and Oslo calendar date explicit, enforce same-tenant composite foreign keys for invoice counterpart/original/voucher references, and correct financial-income signs while excluding result-disposition accounts 8800–8999 from period profit. Migrations 066–070 establish a closed EHF evidence foundation, deterministic projection and a read-only original-status contract. No official rule profile or runtime worker is activated. EHF/Peppol generation in the runtime, transmission, the actual sent payload, transport envelope and authoritative delivery or rejection receipt are NOT implemented. ### Balances GET /klienter/:klientId/saldo/:kontonummer?dato=YYYY-MM-DD&kjent_tid=ISO -> { kontonummer, dato, saldo_oere, saldo_som_kjent_oere? } Positive = debit balance, negative = credit balance. kjent_tid gives the bitemporal answer: "what did we know at that system time". GET /klienter/:klientId/saldobalanse?fra=YYYY-MM-DD&til=YYYY-MM-DD GET /klienter/:klientId/saldobalanse?dato=YYYY-MM-DD -> { fra, til, dato, sum_inngaaende_saldo_oere, sum_periode_debet_oere, sum_periode_kredit_oere, sum_utgaaende_saldo_oere, balanserer, linjer:[{ kontonummer, navn, kontoklasse, kategori, inngaaende_saldo_oere, periode_debet_oere, periode_kredit_oere, utgaaende_saldo_oere, saldo_debet_oere, saldo_kredit_oere }] } Balances are per fiscal year: saldo(konto, dato) = opening balance for that year (once set) + movements in the SAME year up to dato. A year's closing balance becomes next year's opening balance (via year-end close, later). ### General ledger (hovedbok) GET /klienter/:klientId/hovedbok?fra=YYYY-MM-DD&til=YYYY-MM-DD&konto=NNNN -> current posting lines (gyldig_til IS NULL), one row per line, ordered by account then reskontronr then date/voucher: [{ kontonummer, kontonavn, posteringsdato, bilagsnummer, bilagsref, beskrivelse, reskontronr, motpart_navn, debet_oere, kredit_oere }]. reskontronr/motpart_navn are non-null only on reskontro accounts (1500 kunde, 2400 leverandør, ...) — group by reskontronr for a subledger (kundereskontro) view. All filters optional. GET /klienter/:klientId/bilag/:bilagsnummer -> one voucher with its full entry: { bilagsnummer, bilagsref, posteringsdato, beskrivelse, linjer:[{ kontonummer, kontonavn, beskrivelse, reskontronr, motpart_navn, debet_oere, kredit_oere }] }. 404 if unknown. ### Opening balances (inngående balanse) — B1 Opening balances apply only to balance-sheet accounts (class 1-2). They count in saldo() only AFTER the year is "fastsatt" (locked), and locking requires the set to balance (assets = equity + liabilities). POST /klienter/:klientId/inngaaende-balanse/:aar Body: { kontonummer: "1xxx|2xxx", beloep_oere: int>0, dc_sign: 1|-1 } Upserts one opening-balance line. Rejected (422 BOKF) on a result account or after the year is fastsatt (reopen first). -> 201 GET /klienter/:klientId/inngaaende-balanse/:aar -> { aar, fastsatt, avvik_oere, balanserer, linjer:[{kontonummer,navn,beloep_oere,dc_sign}] } avvik_oere = 0 means it balances. POST /klienter/:klientId/inngaaende-balanse/:aar/fastsett { aktoer? } Locks the opening balance. Requires avvik_oere = 0 (else 422 BOKF). -> 201 POST /klienter/:klientId/inngaaende-balanse/:aar/gjenaapne { aktoer? } Reopens a fastsatt opening balance for editing (logged immutably). ### Year-end close (årsavslutning) — B4 GET /klienter/:klientId/aar/:aar/resultat -> { aar, aarsresultat_oere, type:"overskudd"|"underskudd"|"null" }. Computes the year's net result (positive = profit) per 31.12. Changes nothing. POST /klienter/:klientId/aar/:aar/avslutt-regnskap { aktoer? } Closes the fiscal year: (1) posts result allocation on 31.12 (profit: debit 8960 / credit 2050; loss: against 2050 up to available equity, remainder to 2080 Udekket tap), (2) carry-forward — fastsetter next year's opening balance = this year's closing balance, (3) locks the year (reopenable). Tax must be booked BEFORE. -> 201 { status, aar, disponering_postering_id } POST /klienter/:klientId/aar/:aar/gjenaapne-regnskap { aktoer? } Reverses a close: unlocks the year, reverses the allocation voucher (append- only), reopens next year's opening balance. ### Dimensions (prosjekt/avdeling) — B3 Client-defined dimensions tag posting lines via a "dimensjoner" object on each line: { dim_kode: verdi_kode }, e.g. {"prosjekt":"P001","avdeling":"10"}. They flow through drafts and corrections and map to SAF-T . An unknown or inactive dimension/value is rejected (422 BOKF) at booking. POST /klienter/:klientId/dimensjoner { kode:"prosjekt", navn } Creates a dimension type. kode is 1-9 chars [a-z0-9] (used as SAF-T AnalysisType). GET /klienter/:klientId/dimensjoner -> [{ kode, navn, verdier:[{kode,navn,aktiv}] }] POST /klienter/:klientId/dimensjoner/:kode/verdier { verdi_kode, navn } Adds a value to a dimension. Tag a line by adding "dimensjoner" to it in POST /bilag or POST /utkast. ### Statutory exports (validated against Skatteetaten XSDs) GET /klienter/:klientId/saf-t?fra=YYYY-MM-DD&til=YYYY-MM-DD -> SAF-T Financial v1.30 XML (application/xml). Requires kontakt_fornavn/ kontakt_etternavn on the client. GET /klienter/:klientId/mva-melding/:aar/:termin -> VAT return XML (skattemeldingForMerverdiavgift v1.0). termin 1-6 (bi-monthly). ?format=json returns aggregated lines per MVA code instead. GET /klienter/:klientId/mva-avstemming/:aar/:termin -> { beregnet_aa_betale_oere, bokfoert_netto_oere, differanse_oere } Reconciles computed VAT against booked VAT accounts (27xx). differanse_oere must be 0 before filing — non-zero means a manual VAT posting lacks a coded base line. ## Typical agent flow 1. POST /klienter -> klient_id 2. POST /klienter/:id/perioder for each month 3. POST /klienter/:id/bilag for each voucher (set mva_kode on base lines) 4. GET /klienter/:id/mva-avstemming/:aar/:termin -> verify differanse_oere = 0 5. GET /klienter/:id/mva-melding/:aar/:termin -> file VAT return 6. GET /klienter/:id/saf-t?fra=&til= -> audit file on demand