{"openapi":"3.1.0","info":{"title":"Apier.no API","version":"1.0.6","description":"Apier.no is the API that lets AI agents understand and act on Norwegian regulatory obligations. It sits between AI agents and Norwegian government infrastructure (Altinn, Brønnøysund, Skatteetaten) and provides compliance infrastructure: what a company must do legally, who can act on its behalf, and when obligations are due.\n\nAll `/api/v1/*` JSON API endpoints return the standard envelope: { success: true, data: {...}, _meta: {...} } on success, or { success: false, error_code: '...', explanation: {...}, _meta: {...} } on error, with trust metadata (_meta) carrying rulebook version, data freshness, and last-verified timestamp. Plain-text discovery surfaces (`/llms.txt`, `/llms-full.txt`, `/recipes`) and the JSON `/api/health` liveness check are deliberately outside this envelope contract.\n\nAuthentication: Bearer API key in the Authorization header. The full list of operations declared `security: []` (no auth required) is: `/api/health`; `/api/v1/public/deadlines`; `/api/v1/public/obligations`; `/api/v1/tools/exchange-rate`; `/api/v1/tools/altinn-migration`; `/api/v1/capabilities`; `/api/v1/comparison/direct-integration`; `POST /api/v1/privacy/dsr`; `POST /api/v1/account/signup`; and the discovery surfaces `/llms.txt`, `/llms-full.txt`, `/workflows.json`, `/recipes`. Discovery and Category-A surfaces share a soft per-IP cap of 1000/min; the privacy DSR surface drops that to 10/min because it's a transparency surface, not a discovery one; the account-signup surface drops to 5/hour fail-CLOSED per IP because account creation is uniquely abuse-prone (signup spam + email enumeration timing).\n\nChange archive: every detected upstream change from Brønnøysund, Altinn, DigDir, Norges Bank, NAV Aa-registeret, and Skatteetaten Tier 2 (MVA-register + Skatteoppgjør) is persisted to an append-only history and exposed via `GET /api/v1/changes` (Category B, `read:changes` scope). A per-consumer feed remains future work.","contact":{"name":"Apier Support","email":"hello@apier.no","url":"https://apier.no"},"license":{"name":"Proprietary"},"x-schema-org":{"description":"Schema.org structured data for this service is emitted as application/ld+json on every SSR HTML page from `src/app/layout.tsx`. The graph contains Organization, SoftwareApplication, WebAPI, and Service nodes — see `src/lib/seo/json-ld.ts` for the authoritative builder. AI crawlers that read this OpenAPI document can cross-reference by fetching the site root and parsing the application/ld+json block in the document head.","types":["Organization","SoftwareApplication","WebAPI","Service"],"source":"src/lib/seo/json-ld.ts"}},"servers":[{"url":"https://apier.no","description":"Production"},{"url":"https://apier.vercel.app","description":"Preview (Vercel)"}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","description":"API key issued by Apier. Include as: Authorization: Bearer <your-key>"},"AdminBearerAuth":{"type":"http","scheme":"bearer","description":"Admin API key (ADMIN_API_KEY env var). Not the same as consumer API keys."},"SessionCookieAuth":{"type":"apiKey","in":"cookie","name":"sb-access-token","description":"Supabase Auth session cookie set by /auth/callback after a magic-link sign-in. The `name` field above is a documented stand-in; @supabase/ssr v0.10.x in this repo actually emits a per-project pair of cookies prefixed `sb-<project-ref>-auth-token` (split across `-auth-token` and `-auth-token-code-verifier` for the PKCE handshake) — generated OpenAPI clients running in the same-origin browser context send ALL the project's cookies automatically, so a single canonical name suffices for documentation purposes. NOT Bearer-key auth — that schema is BearerAuth (above) and is rejected by withSessionAuth on /api/v1/account/* paths. Required by every /api/v1/account/* endpoint."}},"schemas":{"SandboxMeta":{"type":"object","description":"Sandbox `_meta` block (PR-074). NOT a drop-in for production's `TrustMetadata`; clients must branch on `is_sandbox` before assuming any sandbox/production field equivalence. Differences from production:\n\n  1. `is_sandbox: true` — load-bearing detection signal; `body._meta.is_sandbox === true` is a single property assertion agents use to verify they're talking to the sandbox. Production omits this key entirely.\n  2. `rulebook_version` is the literal string `\"sandbox\"` — production carries the actual Rulebook version (e.g. `\"v1.0.0\"`). Sandbox responses are not Rulebook-evaluated (DECISIONS.md PR-074 §1 Determinism Contract).\n  3. `data_freshness` and `last_verified` are pinned constants (2026-01-01T00:00:00+01:00) — production reflects actual cache / fetch state.\n  4. `response_timestamp` is RFC 3339 with explicit Europe/Oslo offset (`+01:00` CET / `+02:00` CEST, DST-aware) — production's `response_timestamp` carries a UTC `Z` suffix.\n  5. `response_hash` and `source_snapshot_id` are OMITTED — sandbox writes ZERO provenance_log rows (DECISIONS.md PR-074 §2 Middleware Exemptions), so a hash without a matching forensic row would be misleading.\n\nOnly `source` (`\"apier.no\"`) and `schema_version` semantics are shared with production.","additionalProperties":false,"required":["rulebook_version","data_freshness","last_verified","source","schema_version","is_sandbox","response_timestamp"],"properties":{"rulebook_version":{"type":"string","enum":["sandbox"],"description":"Always literal `sandbox` — sandbox responses are not Rulebook-evaluated."},"data_freshness":{"type":"string","format":"date-time","enum":["2026-01-01T00:00:00+01:00"],"description":"Pinned constant (2026-01-01T00:00:00+01:00) for determinism — same value on every call. Schema-locked via `enum` so spec-based drift checks catch any deviation."},"last_verified":{"type":"string","format":"date-time","enum":["2026-01-01T00:00:00+01:00"],"description":"Pinned constant — equal to data_freshness. Schema-locked via `enum` for the same drift-detection reason."},"source":{"type":"string","enum":["apier.no"],"description":"Response origin. Always `apier.no`. Preserves `_meta.source` parity with production `TrustMetadata.source` so clients can reuse `_meta` parsing across both surfaces. Provenance-specific fields (`response_hash`, `source_snapshot_id`) are omitted by design — see DECISIONS.md PR-074 / Sandbox Middleware Exemptions."},"schema_version":{"type":"string","description":"OpenAPI document version — mirrors the top-level `info.version` field. Distinct from `/api/v1/capabilities._meta.schema_version` and `/workflows.json.schema_version`, which version the capabilities-discovery and workflows-discovery surfaces respectively. Equality with `info.version` is enforced at CI time."},"is_sandbox":{"type":"boolean","enum":[true],"description":"Load-bearing sandbox-detection signal. Always `true`."},"response_timestamp":{"type":"string","format":"date-time","description":"RFC 3339 timestamp with explicit Europe/Oslo offset — runtime emits via `formatInTimeZone(now, 'Europe/Oslo', \"yyyy-MM-dd'T'HH:mm:ssXXX\")` so the offset is DST-aware (`+01:00` in CET / `+02:00` in CEST). The ONLY non-deterministic field permitted on sandbox responses (excluded from determinism comparisons — see DECISIONS.md PR-074 / Sandbox Determinism Contract)."}}},"SandboxEnvelopeSuccess":{"type":"object","description":"Sandbox 2xx success envelope BASE shape. Top-level keys mirror the production envelope (`success`, `_meta`) so SDK clients consuming sandbox responses see identical shapes. The `data` field is intentionally omitted from the base — per-operation envelopes (`SandboxContextEnvelope`, `SandboxObligationsEnvelope`, ...) `allOf` this base and pin `data` to the operation's typed payload, so generated clients keep the mirror typing for each route.","required":["success","_meta"],"properties":{"success":{"type":"boolean","enum":[true]},"_meta":{"$ref":"#/components/schemas/SandboxMeta"}}},"SandboxObligation":{"type":"object","description":"Synthetic sandbox obligation. Simplified-but-realistic shape — keys an agent needs to render (id, title, status, required, frequency, deadline_at). Sandbox uses this shape rather than production's `EvaluationResult` because sandbox is for failure-flow + happy-path discovery, not content-fidelity drills (DECISIONS.md PR-074 / Sandbox Determinism Contract).","additionalProperties":false,"required":["obligation_id","title","category","status","required","frequency","legal_reference","deadline_at"],"properties":{"obligation_id":{"type":"string"},"title":{"type":"string"},"category":{"type":"string","enum":["tax","reporting","registration","audit"]},"status":{"type":"string","enum":["applicable","not_applicable","insufficient_data"]},"required":{"type":"string","enum":["always","conditionally","never"]},"frequency":{"type":"string","enum":["annual","quarterly","monthly","bimonthly","one_time"]},"legal_reference":{"type":"string"},"deadline_at":{"type":["string","null"],"format":"date-time","description":"ISO 8601 with explicit Europe/Oslo offset (`+01:00` CET / `+02:00` CEST). Null when no concrete deadline applies (e.g. obligations gated on data the fixture doesn't carry)."}}},"SandboxDeadline":{"type":"object","description":"Synthetic sandbox deadline.","additionalProperties":false,"required":["deadline_id","obligation_id","title","deadline_at","status","category","legal_reference"],"properties":{"deadline_id":{"type":"string"},"obligation_id":{"type":"string"},"title":{"type":"string"},"deadline_at":{"type":"string","format":"date-time","description":"ISO 8601 with explicit Europe/Oslo offset (`+01:00` CET / `+02:00` CEST)."},"status":{"type":"string","enum":["upcoming","overdue","filed"]},"category":{"type":"string","enum":["tax","reporting","registration","audit"]},"legal_reference":{"type":"string"}}},"SandboxAuditEntry":{"type":"object","description":"Synthetic sandbox audit row. Sandbox NEVER writes these to `audit_log` — the array is fabricated for the GET /audit shape (DECISIONS.md PR-074 / Sandbox Middleware Exemptions). The top-level row shape is closed (`additionalProperties: false`); only `details` stays open as an intentional free-form map for per-action context.","additionalProperties":false,"required":["id","timestamp","org_number","action","initiated_by","details"],"properties":{"id":{"type":"string","format":"uuid"},"timestamp":{"type":"string","format":"date-time"},"org_number":{"type":"string","pattern":"^[0-9]{9}$"},"action":{"$ref":"#/components/schemas/AuditAction"},"initiated_by":{"$ref":"#/components/schemas/AuditInitiatedBy"},"details":{"type":"object","additionalProperties":{"oneOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"}]}}}},"SandboxContextData":{"type":"object","description":"Top-level data shape returned by GET /api/v1/sandbox/company/{org}/context. Standalone sandbox-specific shape — NOT a drop-in for the production `CompanyContext` schema. Documented divergences (round 7 — DECISIONS.md PR-074 decision §8): `status` is uppercase enum (`ACTIVE`/`INACTIVE`) where production carries lowercase free text; `data_tier` includes `tier_1_2` for the Tier 1+2 fixture where production exposes the binary `[tier_1, tier_2]` enum; `registration_date` is bare `YYYY-MM-DD` where production carries an ISO 8601 datetime; the `aareg` block carries an additional `note` field that production's `additionalProperties: false` would reject. Sandbox `_meta` is `SandboxMeta` (not `TrustMetadata`). All values are deeply frozen at module load.","additionalProperties":false,"required":["org_number","name","entity_type","nace_codes","status","municipality","registration_date","signaturrett","prokura","data_tier","tier_2","tier_2_note","upgrade_path","aareg","skatteetaten"],"properties":{"org_number":{"type":"string","pattern":"^[0-9]{9}$"},"name":{"type":"string"},"entity_type":{"type":"string","enum":["AS","ENK"]},"nace_codes":{"type":"array","items":{"type":"string"}},"status":{"type":"string","enum":["ACTIVE","INACTIVE"]},"municipality":{"type":"string"},"registration_date":{"type":"string","format":"date"},"signaturrett":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["role","name"],"properties":{"role":{"type":"string"},"name":{"type":"string"}}}},"prokura":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["role","name"],"properties":{"role":{"type":"string"},"name":{"type":"string"}}}},"data_tier":{"type":"string","enum":["tier_1","tier_1_2"]},"tier_2":{"type":["object","null"],"additionalProperties":false,"required":["employee_count","annual_turnover","total_assets","mva_registered","mva_registration_date"],"properties":{"employee_count":{"type":["integer","null"]},"annual_turnover":{"type":["number","null"]},"total_assets":{"type":["number","null"]},"mva_registered":{"type":["boolean","null"]},"mva_registration_date":{"type":["string","null"],"format":"date"}}},"tier_2_note":{"type":["string","null"]},"upgrade_path":{"type":["string","null"]},"aareg":{"type":"object","additionalProperties":false,"required":["available","reason","note"],"properties":{"available":{"type":"boolean","enum":[false]},"reason":{"type":"string","enum":["DELEGATION_MISSING_NAV_AAREG"]},"note":{"type":"string"}}},"skatteetaten":{"type":"object","additionalProperties":false,"required":["mva_register","mva_meldinger","skatteoppgjor"],"description":"Skatteetaten Tier-2 sub-results. Each sub-result mirrors production's `SkatteetatenSubResultUnavailable` shape: when `reason` is `DELEGATION_MISSING` (or `SCOPE_NOT_YET_APPROVED`), the `scope` field carries the Maskinporten scope string the consumer needs to delegate. Sandbox always emits the unavailable branch with `DELEGATION_MISSING` + the canonical scope from `src/lib/skatteetaten/scopes.ts`.","properties":{"mva_register":{"type":"object","additionalProperties":false,"required":["available","reason","scope"],"properties":{"available":{"type":"boolean","enum":[false]},"reason":{"type":"string","enum":["DELEGATION_MISSING"]},"scope":{"type":"string","enum":["skatteetaten:mva-register-read"]}}},"mva_meldinger":{"type":"object","additionalProperties":false,"required":["available","reason","scope"],"properties":{"available":{"type":"boolean","enum":[false]},"reason":{"type":"string","enum":["DELEGATION_MISSING"]},"scope":{"type":"string","enum":["skatteetaten:mva-melding-list-read"]}}},"skatteoppgjor":{"type":"object","additionalProperties":false,"required":["available","reason","scope"],"properties":{"available":{"type":"boolean","enum":[false]},"reason":{"type":"string","enum":["DELEGATION_MISSING"]},"scope":{"type":"string","enum":["skatteetaten:skatteoppgjor-read"]}}}}}}},"SandboxObligationsData":{"type":"object","description":"Top-level data shape returned by GET /api/v1/sandbox/company/{org}/obligations.","additionalProperties":false,"required":["org_number","entity_type","data_tier","obligations","upgrade_path"],"properties":{"org_number":{"type":"string","pattern":"^[0-9]{9}$"},"entity_type":{"type":"string","enum":["AS","ENK"]},"data_tier":{"type":"string","enum":["tier_1","tier_1_2"]},"obligations":{"type":"array","items":{"$ref":"#/components/schemas/SandboxObligation"}},"upgrade_path":{"type":["string","null"]}}},"SandboxDeadlinesData":{"type":"object","description":"Top-level data shape returned by GET /api/v1/sandbox/company/{org}/deadlines.","additionalProperties":false,"required":["org_number","entity_type","data_tier","deadlines"],"properties":{"org_number":{"type":"string","pattern":"^[0-9]{9}$"},"entity_type":{"type":"string","enum":["AS","ENK"]},"data_tier":{"type":"string","enum":["tier_1","tier_1_2"]},"deadlines":{"type":"array","items":{"$ref":"#/components/schemas/SandboxDeadline"}}}},"SandboxSummaryData":{"type":"object","description":"Top-level data shape returned by GET /api/v1/sandbox/company/{org}/summary. Composes the obligations and deadlines arrays into a single envelope alongside the org/entity/tier identifiers — useful for an agent that needs both lists in one call. NOT a stand-in for /context: the company-detail fields (`name`, `nace_codes`, `municipality`, `signaturrett`, `prokura`, `tier_2`, `aareg`, `skatteetaten`) are not included; agents needing those must call /context separately.","additionalProperties":false,"required":["org_number","entity_type","data_tier","obligations","deadlines","upgrade_path"],"properties":{"org_number":{"type":"string","pattern":"^[0-9]{9}$"},"entity_type":{"type":"string","enum":["AS","ENK"]},"data_tier":{"type":"string","enum":["tier_1","tier_1_2"]},"obligations":{"type":"array","items":{"$ref":"#/components/schemas/SandboxObligation"}},"deadlines":{"type":"array","items":{"$ref":"#/components/schemas/SandboxDeadline"}},"upgrade_path":{"type":["string","null"]}}},"SandboxAuditData":{"type":"object","description":"Top-level data shape returned by GET /api/v1/sandbox/company/{org}/audit. Note the double-wrap (`data.data` for the rows + `data.pagination` for the cursor block) — production audit uses the same nesting.","additionalProperties":false,"required":["data","pagination"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/SandboxAuditEntry"}},"pagination":{"type":"object","additionalProperties":false,"required":["limit","has_more"],"properties":{"limit":{"type":"integer","minimum":1,"maximum":200,"description":"Echoed page-size cap. Always emits the production default (50) regardless of any `?limit=` value — sandbox does not paginate (deterministic-fixture model)."},"has_more":{"type":"boolean","enum":[false],"description":"Sandbox pagination is a stub — `has_more` is always false; the fixture's audit_entries array is returned in full. Agents testing pagination logic should hit production."}}}}},"SandboxContextEnvelope":{"unevaluatedProperties":false,"allOf":[{"$ref":"#/components/schemas/SandboxEnvelopeSuccess"},{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/SandboxContextData"}}}]},"SandboxObligationsEnvelope":{"unevaluatedProperties":false,"allOf":[{"$ref":"#/components/schemas/SandboxEnvelopeSuccess"},{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/SandboxObligationsData"}}}]},"SandboxDeadlinesEnvelope":{"unevaluatedProperties":false,"allOf":[{"$ref":"#/components/schemas/SandboxEnvelopeSuccess"},{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/SandboxDeadlinesData"}}}]},"SandboxSummaryEnvelope":{"unevaluatedProperties":false,"allOf":[{"$ref":"#/components/schemas/SandboxEnvelopeSuccess"},{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/SandboxSummaryData"}}}]},"SandboxAuditEnvelope":{"unevaluatedProperties":false,"allOf":[{"$ref":"#/components/schemas/SandboxEnvelopeSuccess"},{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/SandboxAuditData"}}}]},"SandboxExplanation":{"type":"object","description":"Sandbox `explanation` body. Carries the same Norwegian-bokmål Compliance Explainer fields as production's `Explanation` schema, EXCEPT for `error_code` — the sandbox envelope already declares `error_code` at the TOP level (`SandboxEnvelopeError.error_code`), so requiring it again here would force generated clients to expect a duplicate field that the production ApiError shape never had. The optional `details` array carries field-level diagnostics for VALIDATION_FAILED branches (org_number / simulate_error / route-specific parser failures), same `[{field, message}]` shape as `ApiError.explanation.details`.","additionalProperties":false,"required":["summary","why","fix_steps","relevant_link","legal_basis","handover"],"properties":{"summary":{"type":"string","maxLength":140,"description":"One-sentence Norwegian summary suitable for inline display."},"why":{"type":"string","maxLength":240,"description":"Slightly longer Norwegian explanation of the root cause."},"fix_steps":{"type":"array","minItems":2,"maxItems":5,"items":{"type":"string"},"description":"Two to five concrete imperative Norwegian next steps."},"relevant_link":{"type":["string","null"],"format":"uri","description":"Public Altinn / Skatteetaten / Brønnøysund / Apier-docs URL when one applies. Null otherwise."},"legal_basis":{"type":["string","null"],"description":"Lovdata-style legal reference when the error maps to a specific regulatory provision. Null otherwise."},"handover":{"anyOf":[{"$ref":"#/components/schemas/HandoverAction"},{"type":"null"}],"description":"Human handover pointer when the agent cannot resolve the error itself. Null for agent-resolvable errors."},"details":{"type":"array","description":"Optional field-level validation diagnostics. Populated on `error_code: VALIDATION_FAILED` so the parser/Zod message lands alongside the Norwegian explainer text instead of being dropped.","items":{"type":"object","additionalProperties":false,"required":["field","message"],"properties":{"field":{"type":"string","description":"Dotted path to the field that failed (e.g. `org_number`, `simulate_error`, `limit`)."},"message":{"type":"string","description":"Human-readable description of the validation failure (e.g. the Zod issue message)."}}}}}},"SandboxEnvelopeError":{"type":"object","description":"Sandbox 4xx / 5xx error envelope. Top-level keys mirror the production error envelope (`success`, `error_code`, `explanation`, `_meta`). `error_code` is a closed enum of the six codes sandbox can emit, and `explanation` references the typed `SandboxExplanation` schema (production `Explanation` + optional `details`). Generated clients can rely on `summary`, `why`, `fix_steps`, `relevant_link`, `legal_basis`, `handover`, and `details`.","additionalProperties":false,"required":["success","error_code","explanation","_meta"],"properties":{"success":{"type":"boolean","enum":[false]},"error_code":{"type":"string","enum":["AUTH_MISSING_DELEGATION","AUTH_EXPIRED_TOKEN","VALIDATION_FAILED","SCOPE_MISSING","COMPANY_NOT_FOUND","INTERNAL_ERROR"],"description":"Closed enum of six explainer codes the sandbox READ-SIDE GET routes can emit. The `AUTH_*` codes were added in PR-074-sandbox-execute (write-side mirror) — they're shared with the read-side surface because reserved error orgs (999000901-904) trigger them on every sandbox response, including read-side. `INTERNAL_ERROR` is the 500-class fallback for unexpected throws inside the sandbox handler — Sentry IS captured but ZERO audit/provenance rows are written, per DECISIONS.md PR-074 / Sandbox Middleware Exemptions. Iteration-29 polish (CodeRabbit Minor): `REQUEST_TOO_LARGE` removed from this read-side enum — it's emitted ONLY on the write-side 413 path (`readBodyAsTextWithLimit` overflow) and lives on `SandboxWriteSideEnvelopeError` instead. Read-side routes never expose a 413 path."},"explanation":{"$ref":"#/components/schemas/SandboxExplanation"},"_meta":{"$ref":"#/components/schemas/SandboxMeta"}}},"PublicSandboxEnvelopeError":{"type":"object","description":"4xx / 5xx error body envelope for the public-sandbox surface (`/api/v1/sandbox/public/*`). Mirrors `SandboxEnvelopeError` but ADDS the public-surface-only `error_code` values that the internal sandbox never emits: `PUBLIC_SANDBOX_ORG_NOT_PERMITTED` (any 9-digit org other than 999999999 — Rule 24), `PUBLIC_SANDBOX_BODY_TOO_LARGE` (POST body > 32 KiB — the streamed `readBodyAsTextWithLimit` overflow), and `RATE_LIMIT_EXCEEDED` (per-IP bucket exhausted; carries `Retry-After`). The shared `AUTH_*` / `VALIDATION_FAILED` / `SCOPE_MISSING` codes flow through from the `?simulate_error=` failure-injection vocabulary (CLAUDE.md Rule 28) — agents can rehearse the same negative paths on the public mirror they get on /v1/sandbox/*. Generated clients can `switch` exhaustively on this enum.","additionalProperties":false,"required":["success","error_code","explanation","_meta"],"properties":{"success":{"type":"boolean","enum":[false]},"error_code":{"type":"string","enum":["PUBLIC_SANDBOX_ORG_NOT_PERMITTED","PUBLIC_SANDBOX_BODY_TOO_LARGE","RATE_LIMIT_EXCEEDED","VALIDATION_FAILED","AUTH_MISSING_DELEGATION","AUTH_EXPIRED_TOKEN","SCOPE_MISSING","COMPANY_NOT_FOUND","INTERNAL_ERROR"],"description":"Closed enum of error codes the public-sandbox surface can emit. The three `PUBLIC_SANDBOX_*` codes are public-surface-only; the remaining six are shared with the internal sandbox via the `?simulate_error=` vocabulary (Rule 28) plus the boundary-validation and 500 paths."},"explanation":{"$ref":"#/components/schemas/SandboxExplanation"},"_meta":{"$ref":"#/components/schemas/SandboxMeta"}}},"SandboxWriteSideEnvelopeError":{"type":"object","description":"Sandbox 4xx / 5xx error body envelope for the PR-074-sandbox-execute write-side endpoints (`/api/v1/sandbox/auth/approval-token`, `/api/v1/sandbox/actions/plan`). Mirrors `SandboxEnvelopeError` but tightens the `error_code` enum to ONLY the codes the write-side actually emits — `COMPANY_NOT_FOUND` is intentionally excluded because the sandbox write-side invariant returns `400 VALIDATION_FAILED` (not `404`) on unknown orgs (DECISIONS.md PR-074-sandbox-execute § Sandbox write-side org-number invariant). `REQUEST_TOO_LARGE` is included because `readBodyAsTextWithLimit` overflow on these zero-auth POST routes returns a 413 with that code. Iteration-22 polish (CodeRabbit Major Refactor): introduced so the published response contract advertises a closed enum SDK clients can switch on exhaustively. Iteration-23 polish (CodeRabbit Critical): `additionalProperties: false` removed from this base schema so it can compose cleanly via `allOf` from `SandboxExecuteEnvelopeError` (which adds `execution_guarantee` + `outcome` and closes via top-level `unevaluatedProperties: false`). The runtime only emits the four required fields on approval-token + plan, so the downstream effect on those response contracts is documentation-only — SDK generators no longer flag extra fields, but no runtime path emits any.","required":["success","error_code","explanation","_meta"],"properties":{"success":{"type":"boolean","enum":[false]},"error_code":{"type":"string","enum":["AUTH_MISSING_DELEGATION","AUTH_EXPIRED_TOKEN","VALIDATION_FAILED","SCOPE_MISSING","INTERNAL_ERROR","REQUEST_TOO_LARGE"],"description":"Closed enum of explainer codes the sandbox write-side surfaces emit. Six codes — `COMPANY_NOT_FOUND` is excluded (write-side returns `400 VALIDATION_FAILED` on unknown orgs)."},"explanation":{"$ref":"#/components/schemas/SandboxExplanation"},"_meta":{"$ref":"#/components/schemas/SandboxMeta"}}},"SandboxWriteSideEnvelopeErrorClosed":{"description":"Closed wrapper around `SandboxWriteSideEnvelopeError` for the approval-token + plan response refs. The base schema is intentionally open (no `additionalProperties: false`) so `SandboxExecuteEnvelopeError` can compose it via `allOf` without rejecting the PR-A reliability fields — but approval-token + plan don't emit those, so their published contract should be CLOSED. This wrapper composes the base via `allOf` and stamps `unevaluatedProperties: false` at the top level so SDK generators flag any drift from the four runtime fields (`success`, `error_code`, `explanation`, `_meta`). Iteration-25 polish (CodeRabbit Minor): introduced after iter-23 left `SandboxWriteSideError` pointing at the open base schema.","type":"object","unevaluatedProperties":false,"allOf":[{"$ref":"#/components/schemas/SandboxWriteSideEnvelopeError"}]},"SandboxExecuteEnvelopeError":{"description":"Sandbox 4xx / 5xx error body envelope for `/api/v1/sandbox/actions/execute`. Composes `SandboxWriteSideEnvelopeError` with the PR-A reliability fields (`execution_guarantee` + `outcome`) at top level (universal-emission contract — every action response, success or error, ships the reliability envelope so SDK parsers don't have to branch on shape). `unevaluatedProperties: false` keeps the union closed. Iteration-22 polish (CodeRabbit Major Refactor): replaced an inline schema that had widened `error_code` back to an arbitrary `{ type: \"string\" }` and dropped the closed-envelope semantics — composing via `allOf` inherits the constrained `error_code` enum from the base.","unevaluatedProperties":false,"allOf":[{"$ref":"#/components/schemas/SandboxWriteSideEnvelopeError"},{"type":"object","required":["execution_guarantee","outcome"],"properties":{"execution_guarantee":{"$ref":"#/components/schemas/ExecutionGuarantee"},"outcome":{"$ref":"#/components/schemas/Outcome"}}}]},"SandboxExecuteDryRunData":{"type":"object","description":"Discriminated union arm — `dry_run: true`. Returned by `/api/v1/sandbox/actions/execute?dry_run=true`. Carries the deterministic 5-step validation block (`company_exists`, `system_user_authorised`, `scopes_delegated`, `data_format_valid`, `deadline_in_future`). No `receipt`, no `altinn_receipt_id`, no HMAC signature — sandbox dry-run does NOT mint receipts. Disclaimer field warns that dry-run does not guarantee actual submission success.","additionalProperties":false,"required":["dry_run","org_number","action","validation"],"properties":{"dry_run":{"type":"boolean","enum":[true],"description":"Discriminator: always `true` on this union arm."},"org_number":{"type":"string","pattern":"^\\d{9}$"},"action":{"type":"string","enum":["mva_melding","a_melding"]},"validation":{"type":"object","required":["all_passed","checks","disclaimer"],"additionalProperties":false,"properties":{"all_passed":{"type":"boolean"},"checks":{"type":"array","description":"Five-element tuple, fixed order: company_exists, system_user_authorised, scopes_delegated, data_format_valid, deadline_in_future. Iteration-24 polish (CodeRabbit Major): pinned to a JSON Schema 2020-12 `prefixItems` tuple with `minItems: 5` + `maxItems: 5` so the published contract matches the runtime invariant — SDK consumers can switch on positional index instead of searching by name.","minItems":5,"maxItems":5,"prefixItems":[{"type":"object","required":["name","passed","explanation_summary"],"additionalProperties":false,"properties":{"name":{"type":"string","const":"company_exists"},"passed":{"type":"boolean"},"explanation_summary":{"type":"string"}}},{"type":"object","required":["name","passed","explanation_summary"],"additionalProperties":false,"properties":{"name":{"type":"string","const":"system_user_authorised"},"passed":{"type":"boolean"},"explanation_summary":{"type":"string"}}},{"type":"object","required":["name","passed","explanation_summary"],"additionalProperties":false,"properties":{"name":{"type":"string","const":"scopes_delegated"},"passed":{"type":"boolean"},"explanation_summary":{"type":"string"}}},{"type":"object","required":["name","passed","explanation_summary"],"additionalProperties":false,"properties":{"name":{"type":"string","const":"data_format_valid"},"passed":{"type":"boolean"},"explanation_summary":{"type":"string"}}},{"type":"object","required":["name","passed","explanation_summary"],"additionalProperties":false,"properties":{"name":{"type":"string","const":"deadline_in_future"},"passed":{"type":"boolean"},"explanation_summary":{"type":"string"}}}],"items":{"type":"object","$comment":"Permissive `items` schema — required by Spectral oas3 array-items rule. Closure of the tuple is enforced via `minItems: 5` + `maxItems: 5` + the five-element `prefixItems` above; no additional items can exist past index 4."}},"disclaimer":{"type":"string"}}}}},"SandboxExecuteLiveData":{"type":"object","description":"Discriminated union arm — `dry_run: false`. Returned by `/api/v1/sandbox/actions/execute` (no dry_run, or `dry_run=false`) when the approval-token gauntlet passes. Carries the deterministic receipt envelope: HMAC-derived `altinn_receipt_id` (32 hex), UUID v5 `audit_log_id`, ISO-8601 `timestamp` (Europe/Oslo with millisecond precision), `government_response_raw` (sandbox-fixtured upstream payload with the `_sandbox_marker` defense-in-depth field), HMAC-SHA-256 `signature` over the canonical message, and `signature_algorithm: \"HMAC-SHA256-v1\"`. Top-level `_sandbox_marker` is a sibling of `_meta` on the OUTER 200 envelope (set by the route, not on this `data` block) — defense in depth so partial serializations still surface the marker.","additionalProperties":false,"required":["dry_run","receipt"],"properties":{"dry_run":{"type":"boolean","enum":[false],"description":"Discriminator: always `false` on this union arm."},"receipt":{"type":"object","required":["altinn_receipt_id","audit_log_id","timestamp","org_number","filing_type","rule_version","government_response_raw","signature","signature_algorithm"],"additionalProperties":false,"properties":{"altinn_receipt_id":{"type":"string","pattern":"^[a-f0-9]{32}$","description":"32-char lowercase hex — first 32 hex chars of HMAC-SHA-256 over the canonical receipt message."},"audit_log_id":{"type":"string","format":"uuid","description":"UUID v5 derived from the SANDBOX_UUID_NAMESPACE + canonical receipt message (deterministic — same request triple → same UUID)."},"timestamp":{"type":"string","format":"date-time","description":"ISO-8601 with explicit Europe/Oslo offset and millisecond precision (`yyyy-MM-dd'T'HH:mm:ss.SSSXXX`). Pinned to SANDBOX_NOW (deterministic)."},"org_number":{"type":"string","pattern":"^\\d{9}$"},"filing_type":{"type":"string","enum":["mva_melding","a_melding"]},"rule_version":{"type":"string","enum":["sandbox"],"description":"Always the literal `sandbox` — production uses dated versions like `2026.05.01`."},"government_response_raw":{"type":"object","description":"Sandbox-fixtured upstream payload. Top-level `_sandbox_marker` is REQUIRED here (defense-in-depth duplicate of the outer envelope marker). The inner shape varies per upstream (Altinn for filings); sandbox returns a minimal recognisable subset.","required":["_sandbox_marker"],"properties":{"_sandbox_marker":{"type":"string","enum":["SANDBOX_NOT_REAL_GOV_RESPONSE"],"description":"Defense-in-depth marker (also at the outer envelope top level). Agents partial-serializing only the nested upstream payload still see this."},"org_number":{"type":"string"},"action_type":{"type":"string","enum":["mva_melding","a_melding"]},"altinn_status":{"type":"string","enum":["SUBMITTED","PROCESSING","ACCEPTED"]},"altinn_correlation":{"type":"string"},"filing_period":{"type":"string"}}},"signature":{"type":"string","pattern":"^[a-f0-9]{64}$","description":"Hex-encoded HMAC-SHA-256 over `apier:sandbox:receipt:v1\\x1f<altinn_receipt_id>\\x1f<audit_log_id>\\x1f<org_number>\\x1f<action>\\x1f<timestamp>` keyed on SANDBOX_RECEIPT_HMAC_KEY. 64 lowercase hex chars."},"signature_algorithm":{"type":"string","enum":["HMAC-SHA256-v1"]}}}}},"CapabilitiesSandboxBlock":{"type":"object","description":"The `sandbox` block on `GET /api/v1/capabilities` — the agent-discovery surface for the zero-auth sandbox API. The top-level `SandboxSimulateError` parameter description points clients at this block via `/api/v1/capabilities.sandbox.supported_error_codes`; the schema below documents the full shape so generated clients can read every related discovery field (base_url, reserved test orgs, reserved error orgs, supported error codes, CORS posture, determinism contract).","additionalProperties":false,"required":["available","base_url","reserved_test_org_numbers","reserved_error_org_numbers","simulate_error_param","supported_error_codes","cors","determinism"],"properties":{"available":{"type":"boolean","enum":[true],"description":"Always `true` while the sandbox is shipping. A future PR that decommissions or temporarily disables the sandbox would flip this to `false` and document the swap on DECISIONS.md."},"base_url":{"type":"string","format":"uri","description":"Origin-rooted base URL for the sandbox surface, e.g. `https://apier.no/api/v1/sandbox`. Computed from the request origin so preview / local / production deployments each advertise their own base."},"reserved_test_org_numbers":{"type":"array","items":{"type":"string","pattern":"^999000[0-9]{3}$"},"description":"Closed list of synthetic test org numbers the sandbox has fixtures for. All `999000XXX`-prefixed mnemonic — DELIBERATELY fail Brønnøysund MOD-11. Source of truth: `src/lib/sandbox/fixtures.ts`."},"reserved_error_org_numbers":{"type":"object","description":"Map of reserved error org_number → simulated error code. Calling the sandbox with one of these orgs triggers the mapped error response without needing `?simulate_error=`. Currently 999000901-904 → AUTH_MISSING_DELEGATION / AUTH_EXPIRED_TOKEN / VALIDATION_FAILED / SCOPE_MISSING.","additionalProperties":{"type":"string","enum":["AUTH_MISSING_DELEGATION","AUTH_EXPIRED_TOKEN","VALIDATION_FAILED","SCOPE_MISSING"]}},"simulate_error_param":{"type":"string","description":"Human-readable example of the `?simulate_error=` query template, derived from the four lowercase public tokens (CLAUDE.md Rule 28).","example":"?simulate_error=<missing_delegation|invalid_token|validation_error|scope_missing>"},"supported_error_codes":{"type":"array","items":{"type":"string","enum":["missing_delegation","invalid_token","validation_error","scope_missing"]},"description":"Closed list of lowercase public tokens accepted on `?simulate_error=`. Mirrors the runtime `SIMULATED_ERROR_CODES` in `src/lib/sandbox/error-simulation.ts`."},"cors":{"type":"string","enum":["open"],"description":"CORS posture marker. `open` means `Access-Control-Allow-Origin: *` on every sandbox response — production `/api/v1/*` keeps the restrictive default. DECISIONS.md PR-074 / Sandbox CORS Open Policy."},"determinism":{"type":"string","enum":["byte-equivalent except _meta.response_timestamp"],"description":"External determinism contract — scoped to the JSON RESPONSE BODY, not the full HTTP response. Every sandbox response BODY is byte-equivalent across calls EXCEPT `_meta.response_timestamp`. HTTP headers (notably `X-Correlation-ID`, which is per-request UUID v4) are NOT covered — clients writing integration tests should diff the parsed JSON body, not the raw HTTP response. DECISIONS.md PR-074 §1 Determinism Contract."}}},"SkatteetatenSubResultUnavailable":{"type":"object","required":["available","reason"],"additionalProperties":false,"description":"Shared shape for the `available: false` branch of any Skatteetaten sub-result (mva_register, mva_meldinger, skatteoppgjor). The `scope` field is populated for BOTH DELEGATION_MISSING and SCOPE_NOT_YET_APPROVED so consumers have a machine-readable scope identifier in either case (DELEGATION_MISSING means 'request this scope'; SCOPE_NOT_YET_APPROVED means 'this scope is pending approval'). UPSTREAM_UNAVAILABLE remains scope-irrelevant (forbidden via the else clause).","properties":{"available":{"type":"boolean","enum":[false],"description":"Discriminator: false means the sub-result has no usable data. The `reason` field names the specific cause; the consumer reads `reason` and `scope` (when present) to decide remediation."},"reason":{"type":"string","enum":["DELEGATION_MISSING","SCOPE_NOT_YET_APPROVED","UPSTREAM_UNAVAILABLE"],"description":"Machine-readable cause for available=false. DELEGATION_MISSING — no active delegation overlaps the scope this sub-result requires; consumer should POST /api/v1/auth/system-user/delegate with the scope identifier surfaced in the `scope` field. SCOPE_NOT_YET_APPROVED — delegation exists but Maskinporten has not yet approved this specific scope grant; the `scope` field carries the identifier so the consumer knows which approval is pending — wait for approval and retry. UPSTREAM_UNAVAILABLE — Apier or Skatteetaten upstream is reachable-but-failing (timeout, network blip, 5xx, server-wiring bug); consumer should retry rather than re-delegate."},"scope":{"type":"string","description":"Maskinporten scope identifier this sub-result requires. Populated for DELEGATION_MISSING (so consumer knows what scope to request) AND SCOPE_NOT_YET_APPROVED (so consumer knows which pending approval to wait for). Forbidden (JSON-Schema-rejected via the else clause) when reason is UPSTREAM_UNAVAILABLE — that failure mode is scope-irrelevant."}},"if":{"properties":{"reason":{"enum":["DELEGATION_MISSING","SCOPE_NOT_YET_APPROVED"]}}},"then":{"required":["available","reason","scope"]},"else":{"properties":{"scope":false}}},"TrustMetadata":{"type":"object","description":"Metadata included in every response for trust and verifiability.","required":["rulebook_version","data_freshness","last_verified","source","schema_version"],"properties":{"rulebook_version":{"type":"string","description":"Current Rulebook version. Internal rule-version records carry structured origin metadata (`initial_seed` | `human_review` | `lovdata_drift_response` | `audit_finding` | `customer_question` | `legal_correction`) for operator forensics, but that attribution is deliberately NOT exposed on public responses to avoid leaking operational signal.","example":"v1.0.0"},"data_freshness":{"type":"string","format":"date-time","description":"When this data was last fetched (ISO 8601 UTC)."},"last_verified":{"type":"string","format":"date-time","description":"When this data was last verified against source (ISO 8601 UTC)."},"source":{"type":"string","description":"Origin of this data.","example":"apier.no"},"data_source":{"type":"string","description":"Authoritative origin of the underlying data (CLAUDE.md Rule 3 — listed alongside rulebook_version, data_freshness, last_verified, legal_basis for Rulebook-influenced endpoints). Distinct from `source` (the response origin): `data_source` identifies the authoritative data publisher — e.g. 'Brønnøysund Enhetsregisteret + Apier Universal Rulebook'. Present on registry-backed endpoints.","example":"Brønnøysund Enhetsregisteret + Apier Universal Rulebook"},"served_from":{"type":"string","enum":["cache","live","static"],"description":"Cache-aware endpoints only — 'cache' when the response was served from the local copy, 'live' when the gateway just refreshed from the upstream, 'static' when the payload was computed deterministically from pure code with no DB or upstream read (e.g. /v1/public/deadlines). Absent on endpoints that don't carry this semantic."},"stale":{"type":"boolean","description":"Cache-aware endpoints only — true when the upstream was unreachable and a within-limit cached row was served as a fallback. Absent on normal responses."},"schema_version":{"type":"string","description":"OpenAPI contract version at the time of the response; matches `info.version`. Always present on every `/v1/*` response so a future schema break can be traced to the exact rows / audit events that predate / postdate it. Agent-facing machine interfaces (/v1/capabilities, workflows.json) may override with their own narrower surface version; all other endpoints inherit the manifest-level version.","example":"1.0.0"},"legal_basis":{"type":"string","description":"Legal basis under which the response data is reused (CLAUDE.md Rule 3). Present on Rulebook-influenced endpoints (/v1/company/{org}/obligations, /v1/company/{org}/deadlines). Common value: 'NLOD — public registry reuse'.","example":"NLOD — public registry reuse"},"rule_count":{"type":"integer","minimum":0,"description":"Rule-evaluation count — total rules evaluated for this request. Present on endpoints that invoke the Rulebook evaluator (/v1/company/{org}/obligations)."},"applicable_count":{"type":"integer","minimum":0,"description":"Count of rules whose evaluation_result is 'applicable'. Present on evaluator endpoints."},"not_applicable_count":{"type":"integer","minimum":0,"description":"Count of rules whose evaluation_result is 'not_applicable'. Present on evaluator endpoints."},"insufficient_data_count":{"type":"integer","minimum":0,"description":"Count of rules whose evaluation_result is 'insufficient_data' (Tier 2 gated). Present on evaluator endpoints. rule_count = applicable_count + not_applicable_count + insufficient_data_count."},"cache_age_ms":{"type":["integer","null"],"minimum":0,"description":"PR-F cache layer: milliseconds since the cached row was computed, or null on a cache miss (served_from === 'live'). Agents use it alongside data_freshness as a staleness signal. Present on endpoints wired to the PR-F cache (/v1/company/{org}/obligations, /deadlines, /summary, /v1/public/obligations, /v1/public/deadlines)."},"response_time_ms":{"type":"integer","minimum":0,"description":"PR-F speed signal: total server-side processing time in milliseconds, measured via performance.now() at the route boundary. Mirrored on the `X-Apier-Response-Time-Ms` header so agents can key on it without parsing the body. Present on every PR-F-wired endpoint."},"response_timestamp":{"type":"string","format":"date-time","description":"RFC 3339 / ISO 8601 UTC timestamp stamped by the response builder at wire-exit. Mirrored on the `provenance_log.response_timestamp` column by a fire-and-forget writer. Distinct from `data_freshness` (when upstream data was fetched) and `last_verified` (when the citation was verified); this is the moment the envelope left the server. Always populated at runtime — deliberately left out of the `required` list to avoid a cascading contract change that would invalidate every pre-existing example envelope in this spec (CLAUDE.md Rule 10 append-only response contracts). A future revision may promote it into the `required` list alongside a bulk example refresh.","example":"2026-04-24T12:00:00.000Z"},"response_hash":{"type":"string","pattern":"^sha256:[0-9a-f]{64}$","description":"SHA-256 hash of the canonical-JSON form of the envelope with `_meta` stripped before hashing, prefixed with `sha256:`. Mirrored on `provenance_log.response_hash` so a consumer can independently re-hash the bytes they received and compare against what we persisted (integrity receipt). Stripping `_meta` is load-bearing: volatile fields (response_time_ms, cache_age_ms, served_from) and the hash itself would otherwise feed into the digest and break the cache-hit-bytewise-identical invariant.","example":"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"},"source_snapshot_id":{"type":"string","description":"Optional free-text pointer into an upstream sync snapshot the response derives from — e.g. a Brreg sync's `brreg-<ISO>` snapshot id. Absent when the response is not tied to a specific upstream snapshot (capabilities, tools, static deadlines).","example":"brreg-2026-04-24T02:00:00.000Z"},"is_sandbox":{"type":"boolean","enum":[true],"description":"PR-MCP-02b — present and `true` only when the response was generated from sandbox fixture data (currently `ALTINN_MODE=sandbox` on `/api/v1/altinn/list-acting-capacity`). Absent (NOT `false`) on every other response — the absent-vs-false distinction is deliberate so downstream agents branching on `if (response._meta.is_sandbox)` cannot be misled into treating non-sandbox responses as positively-confirmed-not-sandbox. Distinct from the `/v1/sandbox/*` `is_sandbox: true` literal in `SandboxMeta` (which carries the sandbox surface tag); this flag rides through the production `/api/v1/altinn/*` surface to forensically tag a sandbox-mode response that an OEM evaluator received."}}},"RulebookTrustMetadata":{"description":"Trust metadata returned on Rulebook-influenced endpoints (CLAUDE.md Rule 3). Extends the base TrustMetadata by marking the Rulebook-specific fields (`data_source`, `legal_basis`, `schema_version`, `rule_count`, `applicable_count`, `not_applicable_count`, `insufficient_data_count`) as required rather than optional, so generated clients and contract tests treat them as always-present on these endpoints. `last_verified` on this envelope is the MIN `legal_reference_verified_at` across the rules actually evaluated for the request — never newer than the weakest citation — which is strictly stronger than `TrustMetadata.last_verified` (a generic freshness marker).","allOf":[{"$ref":"#/components/schemas/TrustMetadata"},{"type":"object","required":["data_source","legal_basis","schema_version","rule_count","applicable_count","not_applicable_count","insufficient_data_count"]}]},"RulebookErrorTrustMetadata":{"description":"Trust metadata returned on 4xx / 5xx error responses from Rulebook-influenced endpoints. Requires `data_source`, `legal_basis`, and `schema_version` on every error path — including middleware-generated 401 / 429 responses, which the endpoint enriches at the route boundary before returning. Counts (`rule_count`, `applicable_count`, etc.) are NOT required on error responses because rule evaluation never ran.","allOf":[{"$ref":"#/components/schemas/TrustMetadata"},{"type":"object","required":["data_source","legal_basis","schema_version"]}]},"RulebookApiError":{"description":"Error response envelope returned by Rulebook-influenced endpoints. Identical to ApiError but pins `_meta` to RulebookErrorTrustMetadata so generated clients and contract tests see `data_source`, `legal_basis`, and `schema_version` on every failure path (CLAUDE.md Rule 3).","allOf":[{"$ref":"#/components/schemas/ApiError"},{"type":"object","required":["_meta"],"properties":{"_meta":{"$ref":"#/components/schemas/RulebookErrorTrustMetadata"}}}]},"ApiError":{"type":"object","description":"Standard error response envelope returned by all endpoints on failure.","required":["success","error_code","explanation","_meta"],"properties":{"success":{"type":"boolean","enum":[false],"description":"Always false for error responses."},"error_code":{"type":"string","description":"Machine-readable error code.","example":"VALIDATION_FAILED"},"explanation":{"type":"object","description":"Human-readable error details.","required":["summary"],"properties":{"summary":{"type":"string","description":"Short description of the error."},"why":{"type":"string","description":"Detailed reason (in Norwegian where applicable)."},"fix_steps":{"type":"array","items":{"type":"string"},"description":"Steps to resolve the error."},"relevant_link":{"type":"string","format":"uri","description":"Link to relevant documentation."},"legal_basis":{"type":"string","description":"Norwegian legal reference where applicable."},"details":{"type":"array","description":"Field-level validation errors. Populated for error_code=VALIDATION_FAILED when one or more request fields failed schema validation; one entry per failing field.","items":{"type":"object","required":["field","message"],"properties":{"field":{"type":"string","description":"Dotted path to the field that failed (e.g. 'org_number', 'scopes.0')."},"message":{"type":"string","description":"Human-readable description of the validation failure."}}}},"handover":{"$ref":"#/components/schemas/HandoverAction","description":"Human next-step pointer forwarded verbatim from the compliance explainer. Present iff the error requires follow-up the agent cannot perform itself (AUTH_INSUFFICIENT_ROLE, AUTH_NO_DELEGATION, SCOPE_MISSING, UNKNOWN). Absent for errors the agent can resolve by retry, input adjustment, or wait."}}},"_meta":{"$ref":"#/components/schemas/TrustMetadata"}}},"CompanyRoleHolder":{"type":"object","description":"One entry in signaturrett, prokura, or board_members. Roles are stored as metadata (role label + display name), never as national identity numbers — CLAUDE.md Rule 11 (data minimisation).","required":["role","name"],"properties":{"role":{"type":"string","description":"Norwegian role label (e.g. 'Styreleder', 'Prokurist', 'Daglig leder').","example":"Styreleder"},"name":{"type":"string","description":"Display name of the role holder.","example":"Ola Nordmann"},"rule":{"type":"string","description":"Optional signing rule (e.g. 'Alene', 'To i fellesskap').","example":"Alene"}}},"Company":{"type":"object","description":"Tier 1 public company data sourced from Brønnøysund. Served to any caller — the free public surface and the authenticated consumer surface use the same shape. Read-only from the consumer perspective; writes happen through the Registry Engine ingestion pipeline.","required":["org_number","name","entity_type","nace_codes","status","municipality","registration_date","signaturrett","prokura","board_members","last_updated","created_at"],"properties":{"org_number":{"type":"string","pattern":"^[0-9]{9}$","description":"9-digit Norwegian organisation number (Brønnøysund-issued).","example":"123456789"},"name":{"type":"string","description":"Registered company name.","example":"Eksempel AS"},"entity_type":{"type":"string","description":"Norwegian entity type code. Common values: AS (aksjeselskap), ENK (enkeltpersonforetak), NUF (norsk avdeling av utenlandsk foretak), ANS (ansvarlig selskap). Free-text because Brønnøysund publishes ~30 codes in total.","example":"AS"},"nace_codes":{"type":"array","items":{"type":"string"},"description":"NACE industry codes assigned to this company. An array because large entities have multiple; the first entry is the primary activity.","example":["62.010"]},"status":{"type":"string","description":"Brønnøysund status — typically 'active'. May also be 'liquidated', 'bankrupt', 'merged', etc.","example":"active"},"municipality":{"type":"string","nullable":true,"description":"Registered business address municipality. Null when Brønnøysund did not publish one.","example":"Oslo"},"registration_date":{"type":"string","format":"date-time","nullable":true,"description":"ISO 8601 UTC timestamp when the entity was registered with Brønnøysund. Null when the registry did not expose a date."},"signaturrett":{"type":"array","items":{"$ref":"#/components/schemas/CompanyRoleHolder"},"description":"Persons holding signing rights (signaturrett). Roles, not national identity numbers."},"prokura":{"type":"array","items":{"$ref":"#/components/schemas/CompanyRoleHolder"},"description":"Persons holding power of procuration (prokura)."},"board_members":{"type":"array","items":{"$ref":"#/components/schemas/CompanyRoleHolder"},"description":"Current board members including role labels."},"last_updated":{"type":"string","format":"date-time","description":"ISO 8601 UTC timestamp of the most recent ingest that modified this row."},"created_at":{"type":"string","format":"date-time","description":"ISO 8601 UTC timestamp when this row first landed in the Registry."}}},"CompanyTier2Data":{"type":"object","description":"Tier 2 commercial metrics surfaced at /api/v1/company/{org}/context as the `data.tier_2` block. Visible ONLY to consumers with an active delegation for the organisation — gated at the DB level via RLS as well as at the API layer. When the caller has no commercial-tier-2 delegation the response still returns the full envelope but with `data.tier_2 = null`, `data.data_tier = \"tier_1\"`, and `data.tier_2_note` + `data.upgrade_path` populated to point at the delegation flow; the independent `data.aareg` and `data.skatteetaten` blocks remain present according to their own per-block delegation regimes. The schema is exactly the five public commercial fields — the underlying database row also carries server-internal `org_number` (already echoed by the parent envelope) and `last_updated` (row freshness is exposed via `_meta.data_freshness`), but neither is part of the public Tier 2 contract.","required":["employee_count","annual_turnover","total_assets","mva_registered","mva_registration_date"],"additionalProperties":false,"properties":{"employee_count":{"type":["integer","null"],"minimum":0,"description":"Most recent employee count reported to NAV. Null when the Registry has no data yet.","example":42},"annual_turnover":{"type":["integer","null"],"format":"int64","minimum":0,"description":"Most recent annual turnover in NOK (int64 so large enterprises do not overflow).","example":10000000},"total_assets":{"type":["integer","null"],"format":"int64","minimum":0,"description":"Balansesum — total assets on the balance sheet, in NOK. Drives the revisor (auditor) threshold rule (Aksjeloven § 7-6 threshold / Revisorloven § 2-1 audit duty, 27 MNOK). Null until Skatteetaten ingest populates it.","example":5000000},"mva_registered":{"type":["boolean","null"],"description":"Whether the company is registered in the MVA (VAT) register. Null when the Registry does not yet know.","example":true},"mva_registration_date":{"type":["string","null"],"format":"date-time","description":"ISO 8601 UTC timestamp when the company was registered in the MVA register. Null when not applicable."}}},"CompanyContext":{"type":"object","description":"Combined Tier 1 + Tier 2 view returned by company endpoints. `data_tier` is server-computed and tells downstream reasoning code (rulebook / explainer) which fidelity of data it is operating on — 'tier_1' responses carry only public fields, 'tier_2' responses additionally carry the tier_2 object. Agents can branch on data_tier to decide whether to ask for Tier 2 access before proceeding. The block also surfaces `data.aareg` (NAV Aa-registeret employment aggregate) and `data.skatteetaten` (Skatteetaten Tier 2 sharing-API sub-results) at the top level of the response — both INDEPENDENT of `tier_2` (their delegation regimes are scope-isolated; a consumer may hold one without the others). The Tier 1 fields (org_number through prokura) are enumerated explicitly here to mirror the inline /api/v1/company/{org}/context response shape byte-for-byte.","required":["org_number","name","entity_type","nace_codes","status","municipality","registration_date","signaturrett","prokura","data_tier","tier_2","tier_2_note","upgrade_path","aareg","skatteetaten"],"properties":{"org_number":{"type":"string","pattern":"^[0-9]{9}$","description":"9-digit Norwegian organisation number (Brønnøysund-issued).","example":"123456789"},"name":{"type":"string","description":"Registered company name.","example":"Eksempel AS"},"entity_type":{"type":"string","description":"Norwegian entity type code (AS, ENK, NUF, ANS, …).","example":"AS"},"nace_codes":{"type":"array","items":{"type":"string"},"description":"NACE industry codes — primary activity first.","example":["62.010"]},"status":{"type":"string","description":"Brønnøysund status — typically 'active'.","example":"active"},"municipality":{"type":["string","null"],"description":"Registered business address municipality.","example":"Oslo"},"registration_date":{"type":["string","null"],"format":"date-time","description":"ISO 8601 UTC timestamp when the entity was registered with Brønnøysund."},"signaturrett":{"type":"array","items":{"$ref":"#/components/schemas/CompanyRoleHolder"},"description":"Persons holding signing rights."},"prokura":{"type":"array","items":{"$ref":"#/components/schemas/CompanyRoleHolder"},"description":"Persons holding power of procuration (prokura)."},"board_members":{"type":"array","items":{"$ref":"#/components/schemas/CompanyRoleHolder"},"description":"Current board members including role labels. Optional on the /context surface — when absent, callers should not assume the entity has no board (the field's omission reflects that /context historically returned an empty list rather than enriching from a separate board-roster query). Property shape mirrors the canonical Company schema verbatim so generated clients can share the role-holder type across both surfaces."},"tier_2":{"oneOf":[{"$ref":"#/components/schemas/CompanyTier2Data"},{"type":"null"}],"description":"Gated commercial metrics. Null whenever the populated CompanyTier2Data shape cannot be served — the conflated cases are (a) the caller has no active commercial-tier-2 delegation, or (b) the caller has the delegation but the Registry has no Tier 2 row populated yet for this org. Both cases set `data_tier = \"tier_1\"`, `tier_2 = null`, and populate `tier_2_note` + `upgrade_path` with the delegation-flow Norwegian remediation copy. A consumer that already holds delegation may treat the same null-tier_2 + populated-upgrade_path shape as an Apier-side data-not-yet-ingested signal (the next call after the BRREG ingest catches up will surface populated tier_2). A future API revision may add a machine-readable discriminator field to split the two cases; for now the disambiguation lives in the consumer's own delegation-state knowledge."},"tier_2_note":{"type":["string","null"],"description":"Norwegian guidance shown when tier_2 is null — points at POST /api/v1/auth/system-user/delegate. Null when tier_2 is populated."},"upgrade_path":{"type":["string","null"],"description":"Short-form Norwegian sentence pointing at altinn.no/profil + /recipes for the delegation flow. Paired with tier_2_note. Null when tier_2 is populated."},"aareg":{"description":"NAV Aa-registeret aggregate block. Always present on every successful response. `available: false` carries one of three machine-readable reasons; `available: true` carries PII-stripped employment-relationship counts that drive the A-melding obligation, the yrkesskadeforsikring requirement, the revisor employee-count threshold, and the MVA monthly-filing trigger. Aggregate counts only — individual identities (fødselsnummer, names, birth dates) are stripped at the parser boundary and never reach this block.","oneOf":[{"type":"object","required":["available","reason"],"additionalProperties":false,"properties":{"available":{"type":"boolean","enum":[false],"description":"Discriminator: false means no usable upstream data for this caller. The `reason` field carries the machine-readable cause."},"reason":{"type":"string","enum":["DELEGATION_MISSING_NAV_AAREG","SCOPE_NOT_YET_APPROVED","UPSTREAM_UNAVAILABLE"],"description":"DELEGATION_MISSING_NAV_AAREG = no active delegation grants the nav:aareg/v1/arbeidsforhold scope for this org; the consumer should call POST /api/v1/auth/system-user/delegate with the NAV scope. SCOPE_NOT_YET_APPROVED = delegation exists but Maskinporten / NAV have not yet approved the scope grant for this consumer (await approval, then retry). UPSTREAM_UNAVAILABLE = NAV upstream is currently unreachable (timeout, 5xx, network) — the delegation may be fine; the consumer should retry rather than re-delegate."}}},{"type":"object","required":["available","active_count","full_time_count","part_time_count","freelance_count","employment_types","last_checked_at"],"additionalProperties":false,"properties":{"available":{"type":"boolean","enum":[true],"description":"Discriminator: true means the sub-result has fresh upstream data. Counts are populated from the most recent successful NAV fetch (live or cache hit)."},"active_count":{"type":"integer","minimum":0,"description":"Total currently-active employment relationships reported to NAV. Zero is a meaningful state (no employees → A-melding NOT required); never null."},"full_time_count":{"type":"integer","minimum":0,"description":"Subset of active_count classified as full-time (heltid)."},"part_time_count":{"type":"integer","minimum":0,"description":"Subset of active_count classified as part-time (deltid)."},"freelance_count":{"type":"integer","minimum":0,"description":"Subset of active_count classified as freelance / oppdragstaker."},"employment_types":{"type":"array","items":{"type":"string","enum":["full_time","part_time","freelance","apprentice","seasonal"]},"uniqueItems":true,"description":"Distinct employment types present (closed enum, deduplicated). Order is deterministic — same input → same output. `uniqueItems` reflects the parser's normalize step and lets generated clients enforce the invariant client-side."},"last_checked_at":{"type":"string","format":"date-time","description":"RFC 3339 UTC timestamp of the last successful NAV fetch (cache hit OR live fetch)."}}}]},"skatteetaten":{"type":"object","required":["mva_register","mva_meldinger","skatteoppgjor"],"additionalProperties":false,"description":"Skatteetaten Tier 2 sharing-API block. Three INDEPENDENT sub-results (mva_register, mva_meldinger, skatteoppgjor); each gated by its own Maskinporten scope; per-scope isolation means partial scope approval still surfaces data for the approved scopes. Each sub-result reuses the SkatteetatenSubResultUnavailable component for the available:false branch.","properties":{"mva_register":{"description":"MVA-register sub-result. Surfaces current MVA registration status, registration date, and filing frequency.","oneOf":[{"$ref":"#/components/schemas/SkatteetatenSubResultUnavailable"},{"type":"object","required":["available","mva_registered","registration_date","filing_frequency","last_checked_at"],"additionalProperties":false,"properties":{"available":{"type":"boolean","enum":[true],"description":"Discriminator: true means the sub-result has fresh upstream data. The other fields are populated from the most recent successful Skatteetaten MVA-register fetch (live or 24h cache hit)."},"mva_registered":{"type":"boolean","description":"Whether the org is registered in the Norwegian MVA (VAT) register. False is a meaningful state — many ENKs and small AS aren't MVA-registered until omsetning crosses the 50 000 NOK threshold."},"registration_date":{"type":["string","null"],"format":"date-time","description":"ISO 8601 UTC — when the org first entered the MVA register. Null when mva_registered=false (never registered)."},"filing_frequency":{"type":["string","null"],"enum":["arstermin","bimonthly","monthly",null],"description":"Norwegian VAT filing frequency. arstermin = annual (årstermin); bimonthly = default Norwegian VAT (six terminer per year); monthly = high-volume filers. Null when mva_registered=false."},"last_checked_at":{"type":"string","format":"date-time","description":"RFC 3339 UTC timestamp of the last successful MVA-register fetch (live OR cache hit — same value either way; the cache echoes the original fetch timestamp)."}}}]},"mva_meldinger":{"description":"MVA-melding-list sub-result. Surfaces filings_count and filings_skipped_count metadata about MVA returns submitted by the org.","oneOf":[{"$ref":"#/components/schemas/SkatteetatenSubResultUnavailable"},{"type":"object","required":["available","filings_count","filings_skipped_count","last_checked_at"],"additionalProperties":false,"properties":{"available":{"type":"boolean","enum":[true],"description":"Discriminator: true means the sub-result has fresh upstream data. The filings_count + filings_skipped_count fields are echoed from the most recent successful MVA-melding fetch (live or 24h cache hit)."},"filings_count":{"type":"integer","minimum":0,"description":"Number of MVA filings ingested in the most recent successful fetch. Per-filing details are not exposed on any public v1 endpoint; the public surfaces (this block, the obligations endpoint, the deadlines endpoint) carry only derived signals (counts here, evaluated rule outcomes elsewhere)."},"filings_skipped_count":{"type":"integer","minimum":0,"description":"Number of filings the parser dropped because they failed per-filing validation (e.g. missing id). Always 0 for clean upstream responses; non-zero indicates an upstream contract drift worth investigating."},"last_checked_at":{"type":"string","format":"date-time","description":"RFC 3339 UTC timestamp of the last successful MVA-melding fetch (live OR cache hit)."}}}]},"skatteoppgjor":{"description":"Skatteoppgjør (annual tax assessment) sub-result. Surfaces year + last_checked_at metadata. The raw full Skatteetaten payload (skattepliktig inntekt, formue, utlignet skatt, etc.) is NOT exposed on any public v1 endpoint; only evaluated signals derived from the assessment are surfaced.","oneOf":[{"$ref":"#/components/schemas/SkatteetatenSubResultUnavailable"},{"type":"object","required":["available","year","last_checked_at"],"additionalProperties":false,"properties":{"available":{"type":"boolean","enum":[true],"description":"Discriminator: true means the sub-result has fresh upstream data. The year + last_checked_at fields are echoed from the most recent successful Skatteoppgjør fetch (live or 24h cache hit)."},"year":{"type":"integer","minimum":2000,"maximum":2100,"description":"Tax year of the latest assessment Skatteetaten has published for this org. Range bound is sanity-only (the real upstream data starts at 2010+)."},"last_checked_at":{"type":"string","format":"date-time","description":"RFC 3339 UTC timestamp of the last successful Skatteoppgjør fetch (live OR cache hit)."}}}]}}},"data_tier":{"type":"string","enum":["tier_1","tier_2"],"description":"Server-computed fidelity marker. 'tier_2' iff the tier_2 object is populated with at least one non-null core field; 'tier_1' otherwise. Independent of aareg / skatteetaten availability."}}},"AuditPagination":{"type":"object","description":"Keyset-pagination metadata for the audit-trail read surface. The cursor — `next_before` + `next_before_id` — is the (timestamp, id) tuple of the LAST ROW OF THIS RESPONSE'S `data` array; pass both fields back to the next request as `before=` AND `before_id=` to fetch the page strictly older than that tuple. The runtime only emits the cursor inside the `has_more && rows.length > 0` guard, so a terminal page (last row in the result set) lands with both fields absent. The schema enforces the cursor as a tuple at THREE complementary layers: (1) `dependentRequired` rejects the half-tuple shape ({next_before set, next_before_id absent} or vice versa); (2) when `has_more: true`, both fields are REQUIRED via `allOf`/`if/then` (a `has_more: true` response without a cursor is meaningless — the consumer cannot fetch the next page); (3) when `has_more: false`, both fields MUST BE ABSENT — a terminal page that carried a cursor would invite the consumer to make a wasted round-trip.","required":["limit","has_more"],"dependentRequired":{"next_before":["next_before_id"],"next_before_id":["next_before"]},"allOf":[{"if":{"properties":{"has_more":{"const":true}},"required":["has_more"]},"then":{"required":["next_before","next_before_id"]}},{"if":{"properties":{"has_more":{"const":false}},"required":["has_more"]},"then":{"not":{"anyOf":[{"required":["next_before"]},{"required":["next_before_id"]}]}}}],"properties":{"limit":{"type":"integer","minimum":1,"maximum":200,"description":"Echo of the page size in effect for this response."},"has_more":{"type":"boolean","description":"True when older rows beyond this page exist. Computed via the limit+1 lookahead — the (limit+1)-th row is fetched but not returned; only its presence is surfaced here. When true, both `next_before` and `next_before_id` are required; when false, both MUST BE ABSENT (enforced by the schema-level `allOf`/`if/then` clauses)."},"next_before":{"type":"string","format":"date-time","description":"Timestamp of the last row in `data`. Pass back as `before=` for the next page. ABSENT when has_more is false (terminal page); REQUIRED when has_more is true. All-or-nothing tuple companion of `next_before_id`."},"next_before_id":{"type":"string","format":"uuid","description":"Id of the last row in `data`. Pass back as `before_id=` for the next page. ABSENT when has_more is false (terminal page); REQUIRED when has_more is true. All-or-nothing tuple companion of `next_before` — the keyset cursor is (timestamp, id)."}}},"WebhookDeliveryHeaders":{"type":"object","description":"PR-087b — HTTP headers Apier sets on every outbound webhook POST. Documented as a component so consumer SDKs can build a typed receiver, AND referenced from the top-level `webhooks` block (OpenAPI 3.1 outbound-webhook discovery). Signature verification = HMAC-SHA256(`<timestamp>.<raw_body>`, secret) compared against the v1 segment of `X-Apier-Signature`. **Use the 64-character hex `webhook_secret` VERBATIM as the HMAC key — do NOT hex-decode it to 32 raw bytes.** Apier signs the same way (`createHmac('sha256', secret_hex_string)` in node:crypto / equivalent in other runtimes), so a hex-decoded receiver would compute a different MAC and reject every legitimate delivery.","required":["X-Apier-Signature","X-Apier-Correlation-Id","Content-Type","User-Agent"],"properties":{"X-Apier-Signature":{"type":"string","pattern":"^t=\\d+,v1=[a-f0-9]{64}$","description":"Stripe-style HMAC envelope: `t=<unix_seconds>,v1=<hex_hmac>`. Receiver verifies by (1) splitting on the comma, (2) checking `|now - t|` is within tolerance (300s recommended), (3) computing HMAC-SHA256(secret, `<t>.<raw_body>`) and comparing the hex digest to the v1 segment with a constant-time compare.","example":"t=1714234567,v1=2cb9f1a4d8e5b32fa17890c6e4f1d2a9b7e4f0c8d6a3b5f72e9c4a8d6b1f0e3c"},"X-Apier-Correlation-Id":{"type":"string","format":"uuid","description":"UUID v4 stable for the lifetime of the delivery — equal to the `correlation_id` minted at Phase A enqueue time. Useful for joining your receiver-side logs to a `webhook_deliveries` row when escalating through Apier support."},"Content-Type":{"type":"string","enum":["application/json"],"description":"Always application/json. The body is a single `WebhookDeliveryEvent` JSON object."},"User-Agent":{"type":"string","description":"Lightweight identifier so receivers can rate-limit / identify legitimate Apier traffic without parsing the signature.","example":"Apier-Webhooks/1.0"}}},"WebhookDeliveryEvent":{"type":"object","description":"PR-087b — request body shape Apier POSTs to your webhook URL. Documents the contract for consumer SDKs building typed receivers. Signature verification covers the EXACT raw bytes Apier sent — re-stringifying with different key ordering before HMAC compare will fail.","required":["subscription_id","delivered_at","attempt_number","correlation_id","change"],"properties":{"subscription_id":{"type":"string","format":"uuid","description":"Echoes the subscription that produced this delivery — useful for fan-out receivers that maintain multiple consumer-side records."},"delivered_at":{"type":"string","format":"date-time","description":"RFC 3339 UTC timestamp when Apier composed the body (close to but distinct from `t` in the signature header — `t` is the unix-seconds timestamp HMAC'd against the body)."},"attempt_number":{"type":"integer","minimum":1,"maximum":7,"description":"1-indexed attempt counter. 1 = immediate first delivery; 2-7 = retries after 1m / 5m / 15m / 1h / 6h / 24h. Receivers can use this to suppress duplicate side effects after the same `(subscription_id, change.id)` pair fires more than once because the previous attempt's response was lost in transit."},"correlation_id":{"type":"string","format":"uuid","description":"Same UUID as `X-Apier-Correlation-Id`. Repeated in the body so a logging pipeline that strips headers still has the join key."},"change":{"$ref":"#/components/schemas/PublicChangeRow"}}},"SubscriptionFilter":{"type":"object","description":"PR-087b — change-detection webhook subscription filter. Closed key set with implicit AND across keys: a delivery fires only when EVERY present field equals the corresponding column on the change-archive row. No wildcard support today (DECISIONS.md row — exact match keeps the matcher cheap; wildcards would require a regex compile per subscription per change row). At least one key required; an empty object is rejected at the route boundary.","additionalProperties":false,"minProperties":1,"properties":{"source":{"type":"string","enum":["brreg","altinn","digdir","norges_bank","nav","skatteetaten"],"description":"Match the upstream data source (Brønnøysund / Altinn / DigDir / Norges Bank / NAV Aa-registeret / Skatteetaten Tier 2). Same enum the /v1/changes query endpoint uses; CHANGE_SOURCES is the canonical source-of-truth."},"entity_type":{"type":"string","minLength":1,"maxLength":50,"description":"Match the source-specific entity type (e.g. `company` for brreg, `schema` for altinn, `policy` for digdir, `exchange_rate` for norges_bank, `company_tier2` for nav and skatteetaten). Length-capped to keep the GIN filter index tight."},"entity_id":{"type":"string","minLength":1,"maxLength":256,"description":"Match a specific entity id (e.g. an org_number for brreg, a schema_id for altinn). 256-char cap prevents a malicious filter from bloating the GIN index with a long string."},"change_type":{"type":"string","enum":["created","updated","deleted"],"description":"Match the RFC-6902-ish change classification. `created` and `deleted` rows carry full snapshots; `updated` rows are split one-per-changed-field with a JSON Pointer in `field_path`."}}},"SubscriptionCreateRequest":{"type":"object","description":"PR-087b — body of POST /v1/subscriptions. webhook_url MUST be a public HTTPS URL; private CIDRs / .local / metadata-endpoint hosts are rejected. http:// is forbidden in production (NODE_ENV=production gates the WEBHOOKS_ALLOW_HTTP env to false at startup).","required":["webhook_url","filter"],"additionalProperties":false,"properties":{"webhook_url":{"type":"string","format":"uri","minLength":1,"maxLength":2048,"pattern":"^[Hh][Tt][Tt][Pp][Ss]?://","description":"Public HTTP(S) URL. http:// is rejected at runtime when `VERCEL_ENV === \"production\"` (the env-loader superRefine fails-closed there); in non-production environments a Development/Preview deploy can opt into http:// via the `WEBHOOKS_ALLOW_HTTP=true` env var (default false). The schema pattern accepts both http and https case-insensitively (RFC 3986 §3.1 — scheme names are case-insensitive) so a `HTTPS://` spelling from an aggressive normaliser isn't rejected at the contract layer; the runtime gate handles the production-only HTTPS enforcement. Private CIDRs / .local / metadata-endpoint hostnames blocked at create AND at every delivery."},"filter":{"$ref":"#/components/schemas/SubscriptionFilter"}}},"SubscriptionRecord":{"type":"object","description":"PR-087b — public projection of a subscription row. NEVER includes secret material in any form (the SHA-256 hash and AES-256-GCM ciphertext stay server-side; the plaintext was returned exactly once on POST 201 and is unrecoverable thereafter).","required":["id","webhook_url","filter","active","webhook_secret_enc_key_version","created_at"],"additionalProperties":false,"properties":{"id":{"type":"string","format":"uuid","description":"Subscription row identifier — stable for the subscription's lifetime; survives soft-delete (active=false) so the FK to webhook_deliveries (append-only) stays intact for forensic queries."},"webhook_url":{"type":"string","format":"uri","description":"The URL Apier POSTs deliveries to. Validated at create time AND at every delivery against the SSRF blocklist (private CIDRs, .local / .internal suffixes, metadata endpoints, IPv4-mapped IPv6, trailing-dot FQDN spellings). HTTPS-only in production."},"filter":{"$ref":"#/components/schemas/SubscriptionFilter"},"active":{"type":"boolean","description":"False after a soft-delete via DELETE /v1/subscriptions/{id}, after the consecutive-4xx auto-disable threshold, or after admin revocation. Inactive subscriptions are excluded from GET listings; only the lifecycle-history columns (deactivated_at, deactivation_reason) and the webhook_deliveries FK survive."},"webhook_secret_enc_key_version":{"type":"integer","minimum":1,"description":"Encryption key version under which the secret ciphertext is stored. v1 today (reads `WEBHOOK_SECRET_ENC_KEY` env). A future rotation is additive — adding `WEBHOOK_SECRET_ENC_KEY_V2` env + a one-line dispatcher in `encryption.ts` — without altering existing rows."},"created_at":{"type":"string","format":"date-time","description":"RFC 3339 timestamp when the subscription was created. Stable; never updated by lifecycle changes (use `updated_at` if you need to track soft-delete time, exposed via the audit log)."}}},"SubscriptionCreateResponse":{"type":"object","description":"PR-087b — POST 201 response. The `webhook_secret` field carries the plaintext secret returned EXACTLY ONCE — Apier persists only the SHA-256 hash + an AES-256-GCM ciphertext (key-version-tagged), so the plaintext cannot be recovered from the server after this response leaves the wire. Store it client-side immediately.","required":["id","webhook_url","filter","webhook_secret","webhook_secret_enc_key_version","created_at"],"additionalProperties":false,"properties":{"id":{"type":"string","format":"uuid","description":"Newly created subscription identifier — pass to DELETE /v1/subscriptions/{id} to deactivate."},"webhook_url":{"type":"string","format":"uri","description":"Echoes back the URL the consumer submitted, after SSRF validation has accepted it."},"filter":{"$ref":"#/components/schemas/SubscriptionFilter"},"webhook_secret":{"type":"string","pattern":"^[a-f0-9]{64}$","description":"32-byte CSPRNG hex secret used to sign every webhook delivery. Apier emits HMAC-SHA256(`<unix_seconds>.<raw_body>`, secret) on the `X-Apier-Signature` header (Stripe-style `t=<unix>,v1=<hex>`). **Use this 64-character hex string VERBATIM as the HMAC key when verifying — do NOT hex-decode it to 32 raw bytes.** Apier signs with `createHmac('sha256', secret_hex_string)` so a receiver that decodes first computes a different MAC and rejects every legitimate delivery. Returned exactly once — store securely client-side."},"webhook_secret_enc_key_version":{"type":"integer","minimum":1,"description":"Encryption key version this row was minted under. Mirrors the `subscriptions.webhook_secret_enc_key_version` column; useful if you support rotating secrets and need to know which generation a subscription belongs to."},"created_at":{"type":"string","format":"date-time","description":"RFC 3339 timestamp of the row insert (the moment the secret started being valid)."}}},"SubscriptionListResponse":{"type":"object","description":"PR-087b — GET /v1/subscriptions response payload (the `data` field of the standard envelope). Returns active subscriptions belonging to the calling consumer, ordered by created_at DESC. Not paginated: the per-consumer active-subscription cap is 10, so the response always fits in one page.","required":["data"],"additionalProperties":false,"properties":{"data":{"type":"array","maxItems":10,"items":{"$ref":"#/components/schemas/SubscriptionRecord"},"description":"All active subscriptions for this consumer. Capped at the per-consumer limit of 10 active subscriptions, so this array is always exhaustive — no follow-up page request is needed."}}},"PublicChangeRow":{"type":"object","description":"Public projection of one row in the change archive. Sources: `brreg` (Brønnøysundregistrene company facts), `altinn` (Altinn 3 service schemas), `digdir` (DigDir policy documents), `norges_bank` (exchange + interest rates), `nav` (Aa-registeret aggregate tier-2 changes — entity_type='company_tier2', PII-stripped), and `skatteetaten` (Skatteetaten Tier 2 sub-result diffs from MVA-register + Skatteoppgjør — entity_type='company_tier2', financial-figure-redacted). `correlation_id` is INTERNAL to Apier's forensic pipeline and is INTENTIONALLY excluded from this surface — exposing it would let consumers correlate Apier's per-request thread across endpoints, a privacy / ops concern. `source_snapshot_id` IS exposed because it joins to consumer-receipt provenance via the publisher's source_snapshot_id column.","required":["id","source","entity_type","entity_id","change_type","field_path","before_value","after_value","diff","detected_at","source_snapshot_id"],"properties":{"id":{"type":"string","format":"uuid","description":"Row identifier — also the deterministic tiebreaker in the (detected_at, id) ordering used for keyset pagination."},"source":{"type":"string","enum":["brreg","altinn","digdir","norges_bank","nav","skatteetaten"],"description":"Upstream data source. `nav` carries Aa-registeret aggregate tier-2 changes. `skatteetaten` carries MVA-register + Skatteoppgjør sub-result diffs (financial figures stripped at the publisher boundary, never persisted to the diff blob)."},"entity_type":{"type":"string","description":"Source-specific entity type (e.g., 'company' for brreg, 'schema' for altinn, 'policy' for digdir, 'exchange_rate' for norges_bank, 'company_tier2' for nav and skatteetaten)."},"entity_id":{"type":"string","description":"Source-specific entity id (e.g., org_number for brreg)."},"change_type":{"type":"string","enum":["created","updated","deleted"],"description":"Type of change. `created` and `deleted` rows carry NULL field_path + full snapshots; `updated` rows are split one-per-field with a JSON Pointer in field_path."},"field_path":{"type":["string","null"],"description":"RFC 6901 JSON Pointer (`/<field>`) when change_type is `updated`; null for `created` / `deleted`."},"before_value":{"description":"JSONB before-state. Object for full snapshots, single-field projection for `updated` rows, null for `created`. Caller deserializes — shape is source-specific."},"after_value":{"description":"JSONB after-state. null for `deleted`; otherwise per the same source-specific shape as before_value."},"diff":{"type":"array","items":{"type":"object","additionalProperties":true},"description":"RFC 6902 JSON Patch array. Always an array per the DB-level CHECK on the changes table. Each element is a JSON Patch operation object (op + path + optional value)."},"detected_at":{"type":"string","format":"date-time","description":"RFC 3339 detection time. Stable ordering primary key."},"source_snapshot_id":{"type":"string","description":"Per-poll-run identifier — joins to provenance_log.source_snapshot_id for the receipt of the run that produced this row."}}},"DsrField":{"type":"object","description":"Per-field annotation tuple returned by the DSR endpoint. Carries the GDPR transparency triad inline so the data subject sees `data_source` / `legal_basis` / `retention_period` without cross-referencing /privacy or /trust. Array-backed columns on the underlying `companies` table (`nace_codes`, `signaturrett`, `prokura`, `board_members`) are joined into a comma-separated string by the DSR endpoint before annotation — `value` is therefore `string | null` for every key in the fields map.","required":["value","data_source","legal_basis","retention_period"],"properties":{"value":{"type":["string","null"],"description":"The cached value for this field, joined to a comma-separated string for array-backed columns. `null` when the row exists but the field is empty (e.g. a company with no `municipality`, or a `signaturrett` array with zero entries)."},"data_source":{"type":"string","description":"Where the data originated. Today always Brønnøysund (data.brreg.no) — Apier does not enrich Tier 1 records from any other source per Rule 11."},"legal_basis":{"type":"string","description":"GDPR lawful-basis disclosure for this field. References Apier's documented Art 6(1)(f) Legitimate Interest Assessment."},"retention_period":{"type":"string","description":"Plain-language description of how long this field is retained and the trigger that removes it."}}},"DsrCompanyRecord":{"type":"object","description":"Annotated `companies` row returned by the DSR endpoint when an `org_number` query matches. The top-level `org_number` and `company_name` are container identifiers naming WHICH company this record describes — the `fields` map carries the GDPR transparency triad per substantive field (the structure mirrors the data subject's question 'what fields do you hold about my company, and on what basis?').","required":["org_number","company_name","fields"],"properties":{"org_number":{"type":"string","pattern":"^[0-9]{9}$","description":"9-digit Norwegian organisation number — echoes the queried `org_number`. This is the container identifier; the per-field GDPR triad lives inside `fields`."},"company_name":{"type":"string","description":"Display name of the legal entity, as cached from Brønnøysund. Container identifier alongside `org_number`."},"fields":{"type":"object","description":"Per-field map of every public-facing column on the `companies` row. The eight keys below are emitted on every `org_number` query — `additionalProperties` stays open so a future column addition is an append-only contract change rather than a /v2 break, but no key beyond the enumerated set is emitted today.","required":["name","entity_type","nace_codes","status","municipality","signaturrett","prokura","board_members"],"properties":{"name":{"allOf":[{"$ref":"#/components/schemas/DsrField"}],"description":"Cached legal entity display name (companies.name). String-typed; never null today, but the schema permits null to keep the per-field shape uniform."},"entity_type":{"allOf":[{"$ref":"#/components/schemas/DsrField"}],"description":"Norwegian entity type (AS / ENK / NUF / ANS / DA / ...). String."},"nace_codes":{"allOf":[{"$ref":"#/components/schemas/DsrField"}],"description":"Comma-separated NACE industry codes joined from the array column. Null when the row has no codes."},"status":{"allOf":[{"$ref":"#/components/schemas/DsrField"}],"description":"Brønnøysund status (Aktiv / Slettet / Konkurs / ...). String."},"municipality":{"allOf":[{"$ref":"#/components/schemas/DsrField"}],"description":"Norwegian municipality name (companies.municipality). Null when Brønnøysund has no value."},"signaturrett":{"allOf":[{"$ref":"#/components/schemas/DsrField"}],"description":"Comma-separated names of signaturrett-holders. Null on a row with no signaturrett entries. Names only — role labels and signing rules are NOT exposed here per the per-row data minimisation contract."},"prokura":{"allOf":[{"$ref":"#/components/schemas/DsrField"}],"description":"Comma-separated names of prokura-holders. Same minimisation as `signaturrett`."},"board_members":{"allOf":[{"$ref":"#/components/schemas/DsrField"}],"description":"Comma-separated names of board members. Same minimisation as `signaturrett`."}},"additionalProperties":{"$ref":"#/components/schemas/DsrField"}}}},"DsrRoleAttestation":{"type":"object","description":"One (org × role) match returned by the DSR endpoint when a `name` query hits a stored signaturrett / prokura / board_members entry. Each attestation describes a single finding — `org_number` + `company_name` + `role` + `matched_name` together identify it; the GDPR transparency triad annotates the FINDING (not the individual identifier fields) because the legal-basis disclosure is uniform across all four identifier components of one row.","required":["org_number","company_name","role","matched_name","data_source","legal_basis","retention_period"],"properties":{"org_number":{"type":"string","pattern":"^[0-9]{9}$","description":"9-digit Norwegian organisation number of the company where the role match was found."},"company_name":{"type":"string","description":"Display name of the company, as cached from Brønnøysund."},"role":{"type":"string","enum":["signaturrett","prokura","board_member"],"description":"Which role bucket the matched name was found in. Each bucket is scanned independently — a single name can produce multiple attestations across roles (e.g. a board chair who also holds signaturrett returns two attestations on the same company)."},"matched_name":{"type":"string","description":"Display name as stored on the JSONB row that matched. Returned verbatim so the requester sees the exact value Apier holds — case-canonical correction goes through Brønnøysund (see `how_to_request_correction` on the response envelope)."},"data_source":{"type":"string","description":"Where this attestation's data originated — today always Brønnøysund (data.brreg.no). Apier does not enrich Tier 1 records from any other source per the data minimisation rule."},"legal_basis":{"type":"string","description":"GDPR lawful-basis disclosure for this attestation. References Apier's documented Art 6(1)(f) Legitimate Interest Assessment."},"retention_period":{"type":"string","description":"Plain-language description of how long this attestation is retained and the trigger that removes it (typically: until the parent company is deregistered in Brønnøysund, plus a 30-day deletion sync window)."}}},"CompanyDeltaLogEntry":{"type":"object","description":"One row in the append-only company_delta_log. Produced every time the Registry Engine detects a field change during re-ingest. Entries are never updated or deleted at the DB level — a trigger enforces the append-only invariant.","required":["id","entity_id","field","timestamp","source"],"properties":{"id":{"type":"string","format":"uuid","description":"Row identifier."},"entity_id":{"type":"string","pattern":"^[0-9]{9}$","description":"Organisation number the changed field belongs to.","example":"123456789"},"field":{"type":"string","description":"Name of the field that changed (e.g. 'name', 'status', 'employee_count').","example":"status"},"old_value":{"type":"string","nullable":true,"description":"Previous value as text. Null when the change was an addition."},"new_value":{"type":"string","nullable":true,"description":"New value as text. Null when the change was a removal."},"timestamp":{"type":"string","format":"date-time","description":"ISO 8601 UTC timestamp when the delta was recorded."},"source":{"type":"string","description":"Originating upstream — 'brreg' (Brønnøysund), 'skatteetaten', etc.","example":"brreg"}}},"ApiErrorWithStrictProvenance":{"description":"Error envelope tightened with the response-provenance fields (`response_timestamp`, `response_hash`) marked REQUIRED at the schema level. Mirrors the central `ApiError` envelope but adds an `_meta` `allOf` override so generated clients see the strict-provenance contract on shared `Unauthorized`, `RateLimitExceeded`, `PublicRateLimitExceeded`, `IdempotencyInProgress`, and `IdempotencyMismatch` responses. Endpoints that still $ref bare `ApiError` for endpoint-specific 4xx remain backward-compatible until a future global tightening promotes the required fields into the central `ApiError._meta` schema for every endpoint.","allOf":[{"$ref":"#/components/schemas/ApiError"},{"type":"object","properties":{"_meta":{"allOf":[{"$ref":"#/components/schemas/TrustMetadata"},{"type":"object","required":["response_timestamp","response_hash","schema_version"]}]}}}]},"IdempotencyInProgressBody":{"description":"PR-024 — narrowed body schema for HTTP 409 IDEMPOTENCY_IN_PROGRESS, used inside oneOf compositions where the discriminator must be unambiguous. Extends `ApiErrorWithStrictProvenance` with a literal `error_code` constraint so the JSON Schema validator can distinguish this branch from `Conflict409Body` (which has a different literal). Round-2 fix (CodeRabbit Major) — the earlier oneOf used the bare `ApiErrorWithStrictProvenance` schema, which accepts any `error_code` and therefore matched both branches non-exclusively.","allOf":[{"$ref":"#/components/schemas/ApiErrorWithStrictProvenance"},{"type":"object","required":["error_code"],"properties":{"error_code":{"type":"string","enum":["IDEMPOTENCY_IN_PROGRESS"]}}}]},"UpstreamUnavailableBody":{"description":"PR-073 — narrowed body schema for HTTP 503 UPSTREAM_UNAVAILABLE (idempotency reservation service unavailable). Used inside oneOf compositions where the discriminator must be unambiguous. Round-2 fix (CodeRabbit Major).","allOf":[{"$ref":"#/components/schemas/ApiError"},{"type":"object","required":["error_code"],"properties":{"error_code":{"type":"string","enum":["UPSTREAM_UNAVAILABLE"]}}}]},"AltinnLiveSubmitterNotImplementedBody":{"description":"PR-077 — narrowed body schema for HTTP 503 ALTINN_LIVE_SUBMITTER_NOT_IMPLEMENTED (server-side rollout misconfiguration). Used inside oneOf compositions where the discriminator must be unambiguous. Round-2 fix (CodeRabbit Major).","allOf":[{"$ref":"#/components/schemas/ApiError"},{"type":"object","required":["error_code"],"properties":{"error_code":{"type":"string","enum":["ALTINN_LIVE_SUBMITTER_NOT_IMPLEMENTED"]}}}]},"Conflict409Body":{"description":"PR-AGENT-CONFLICT-LOCK (Amendment 61 §5.1) — body schema for HTTP 409 from `POST /api/v1/actions/execute` and `POST /api/v1/auth/system-user/delegate` when the cross-agent write-conflict gate fires. Extends `ApiErrorWithStrictProvenance` with the spec-literal top-level fields (`error`, `first_correlation_id`, `retry_after_seconds`) AND the PR-A reliability fields (`execution_guarantee`, `outcome`) that the runtime adds via `preOrchestratorErrorResponse` (round-1 fix). Generated clients reading this schema see the actual wire shape.","allOf":[{"$ref":"#/components/schemas/ApiErrorWithStrictProvenance"},{"type":"object","required":["error","first_correlation_id","retry_after_seconds","execution_guarantee","outcome"],"properties":{"error":{"type":"string","enum":["concurrent_write_in_flight"],"description":"Amendment 61 §5.1 spec literal. Distinct from `error_code` (which is the Compliance Explainer machine-readable code) so machine clients can switch on `error` without a CAPS-vs-snake parser."},"error_code":{"type":"string","enum":["CONCURRENT_WRITE_IN_FLIGHT"]},"first_correlation_id":{"type":"string","format":"uuid","description":"Cross-vendor multi-agent coordination signal per Amendment 61 §5.1. Strictly a correlation/escalation/forensic signal — it does NOT enable the blocked second agent to fetch the original receipt, since receipt replay requires possessing the original Idempotency-Key (only the original consumer controls that key). Use this value to: (a) wait `retry_after_seconds` and retry with a fresh Idempotency-Key, or (b) escalate to a human operator if two distinct agents under the caller's control are racing on the same action."},"retry_after_seconds":{"type":"integer","minimum":1,"description":"Seconds until the conflict-lock TTL expires for the active blocker. Mirrors the `Retry-After` header value. Always ≥ 1 (server clamps via GREATEST(1, CEIL(...)))."},"execution_guarantee":{"$ref":"#/components/schemas/ExecutionGuarantee","description":"PR-A reliability snapshot synthesised by `preOrchestratorErrorResponse` — the conflict-lock 409 fires BEFORE the orchestrator runs, so this carries `circuit_state: \"CLOSED\"` (a write conflict tells us nothing about gov-API health)."},"outcome":{"$ref":"#/components/schemas/Outcome","description":"PR-A reliability outcome. `is_final: false` + `requires_followup: true` + `followup_action: \"retry\"` because the conflict is transient — a retry after `retry_after_seconds` with a fresh Idempotency-Key is the recommended path for a blocked second agent. The same-consumer original-Idempotency-Key replay path is documented on `first_correlation_id` (it depends on already controlling that key, which the response does NOT expose)."}}}]},"SafetyUnavailable503Body":{"description":"PR-AGENT-CONFLICT-LOCK (Amendment 61 §5.1) — body schema for HTTP 503 when the conflict-lock RPC infrastructure is unavailable (fail-CLOSED). Extends `ApiErrorWithStrictProvenance` with the spec-literal `error` field that mirrors `error_code`, AND the PR-A reliability fields (`execution_guarantee`, `outcome`) that the runtime adds via `preOrchestratorErrorResponse` (round-1 fix). Distinct from `UPSTREAM_UNAVAILABLE` (idempotency reservation infra) and `ALTINN_LIVE_SUBMITTER_NOT_IMPLEMENTED` (submitter activation gate).","allOf":[{"$ref":"#/components/schemas/ApiErrorWithStrictProvenance"},{"type":"object","required":["error","execution_guarantee","outcome"],"properties":{"error":{"type":"string","enum":["safety_system_unavailable"]},"error_code":{"type":"string","enum":["SAFETY_SYSTEM_UNAVAILABLE"]},"execution_guarantee":{"$ref":"#/components/schemas/ExecutionGuarantee"},"outcome":{"$ref":"#/components/schemas/Outcome"}}}]},"RulebookApiErrorWithStrictProvenance":{"description":"Combines `RulebookApiError`'s Rulebook trust metadata (data_source + legal_basis + schema_version REQUIRED) with `ApiErrorWithStrictProvenance`'s response-provenance fields (response_timestamp + response_hash REQUIRED). Used by `RulebookRateLimitExceeded` so Rulebook-influenced endpoints' 429 responses carry BOTH the Rule-3 trust metadata AND the Amendment 001 strict-provenance contract — neither side is weakened. Generated clients on /v1/company/{org}/obligations + /v1/company/{org}/summary 429s see the union of both shapes.","allOf":[{"$ref":"#/components/schemas/ApiError"},{"type":"object","required":["_meta"],"properties":{"_meta":{"allOf":[{"$ref":"#/components/schemas/TrustMetadata"},{"type":"object","required":["data_source","legal_basis","schema_version","response_timestamp","response_hash"]}]}}}]},"AuditAction":{"type":"string","description":"Closed enumeration of audit_log action identifiers — single source of truth for both response bodies and query-parameter filters on the audit-trail read surface. Mirrors `AUDIT_ACTIONS` in src/lib/audit/types.ts. Adding a new value is a one-line append HERE that propagates via $ref to every consumer. Append-only — renaming a value is a /v2 break.","enum":["delegation.create","delegation.revoke","approval_token.mint","approval_token.used","api_key.create","api_key.revoke","gov_api.call","registry.upsert","system.cron_run","privacy.dsr","subscription.create","subscription.delete","webhook.delivered","dry_run_execute","risk_elevated_response","risk_elevated_shadow","risk_pattern_override_applied","submit_mva_started","submit_mva","submit_mva_failed","execute_authorization_failed","rate_limit_blocked","idempotency_replay","altinn.list_acting_capacity","brreg.get_company_profile","write_conflict_blocked"]},"AuditInitiatedBy":{"type":"string","description":"Closed enumeration of `initiated_by` classifier values — single source of truth for both response bodies and query-parameter filters on the audit-trail read surface. Mirrors `INITIATED_BY_VALUES` in src/lib/audit/initiator.ts AND the migration-019 CHECK constraint on audit_log.initiated_by. `human` = write action confirmed via the X-Approval-Token UI flow. `agent` = self-identifying AI client OR an unattended write without approval. `cron` = scheduled job hitting /api/cron/* with the configured CRON_SECRET. `system` = reserved for future internal service-to-service calls. `unknown` = read verbs without other signals, plus the historical-row backfill default for pre-correlation-id rows.","enum":["human","agent","cron","system","unknown"]},"AuditLogEntry":{"type":"object","description":"One row in the append-only audit_log (Sporingslogg) as projected onto the consumer-scoped read surface (GET /api/v1/company/{org}/audit). Written for every state-changing action touching government APIs, approval tokens, or API-key lifecycle. Append-only is enforced at three independent database layers (RLS policy set, BEFORE UPDATE OR DELETE trigger, revoked UPDATE/DELETE grants) so not even a compromised service-role client can rewrite history. The reader projects ALL columns the consumer needs to reconstruct the activity slice — id, timestamp, org_number, action, gov_api_response_status, rule_version, system_user_id, correlation_id, initiated_by, schema_version, details — but DELIBERATELY EXCLUDES `consumer_id`. The consumer is the auth boundary, not data echoed back; surfacing it would be redundant (every row in the response shares the same consumer_id by construction). Sensitive keys in `details` are AUTO-REDACTED to `[REDACTED]` at TWO defence layers: the writer-side redactor (src/lib/audit/redact.ts) before INSERT, and the read-side scrubber (src/lib/audit/scrubber.ts) before the row leaves the API — belt-and-suspenders against pre-existing rows from before the writer-side redaction shipped, and any future writer-side bug. Retention is intentionally eternal for legal/compliance reasons; the standard log-retention pipeline applies to other tables only.","required":["id","timestamp","org_number","action","gov_api_response_status","rule_version","system_user_id","correlation_id","initiated_by","schema_version","details"],"properties":{"id":{"type":"string","format":"uuid","description":"Row identifier (audit_log.id) — also the deterministic tiebreaker in the (timestamp, id) ordering used for keyset pagination on the read surface."},"timestamp":{"type":"string","format":"date-time","description":"ISO 8601 UTC timestamp when the action was recorded. TIMESTAMPTZ on the column; serialised with explicit timezone."},"org_number":{"type":"string","pattern":"^[0-9]{9}$","description":"9-digit Norwegian organisation number this audit row was written for. Non-nullable on this read surface — although the underlying audit_log column IS nullable to allow system-scoped rows (api_key.create, system.cron_run) that have no org context, the GET /api/v1/company/{org}/audit reader filters on `eq('org_number', <url-org>)` so a null value cannot pass through this projection. Generated clients see the tightened contract and do not need a dead null branch. A future endpoint that exposes system-scoped rows would need its own response schema (or wrap AuditLogEntry with `oneOf` to re-permit null).","example":"123456789"},"action":{"allOf":[{"$ref":"#/components/schemas/AuditAction"}],"description":"Machine-readable action identifier. Closed enumeration — see the shared `AuditAction` schema for the full vocabulary. Additions are one-line changes in src/lib/audit/types.ts AUDIT_ACTIONS + the shared schema; new entries are append-only at the type level too (Rule 10).","example":"delegation.create"},"gov_api_response_status":{"type":["integer","null"],"description":"HTTP status from an upstream government API call, when applicable. Null for non-gov actions.","example":201},"rule_version":{"type":["string","null"],"description":"Version of the Rulebook under which the action was evaluated, when the action was rule-driven. Null when the action did not invoke the Rulebook."},"system_user_id":{"type":["string","null"],"description":"Altinn 3 System User identifier, when the action pertains to one. Null for non-Altinn actions."},"correlation_id":{"type":"string","format":"uuid","description":"Request-scope UUID that joins this audit row to other artefacts from the same HTTP request — provenance_log.correlation_id, evaluation_snapshots.correlation_id, compliance_state_events.correlation_id, api_audit_log.correlation_id. Threaded by src/middleware/correlation.ts on every inbound /api/v1/* request; consumers can use it to reconstruct the full forensic slice of one HTTP call across all four logs."},"initiated_by":{"allOf":[{"$ref":"#/components/schemas/AuditInitiatedBy"}],"description":"Who triggered the action — see the shared `AuditInitiatedBy` schema for the full enum semantics. Mirrors the migration-019 CHECK constraint on audit_log.initiated_by."},"schema_version":{"type":"string","description":"OpenAPI contract version at the moment of the audit write — sourced from src/data/openapi.json `info.version` via src/lib/api/schema-version.ts. Lets a future schema break be traced to the audit rows that predate / postdate it."},"details":{"type":"object","description":"Action-specific JSONB metadata. AUTO-REDACTED at two layers — writer-side (src/lib/audit/redact.ts) before INSERT, and read-side (src/lib/audit/scrubber.ts) before the row leaves the API. Any key (case-insensitive substring) containing token / secret / password / bearer / authorization / jwt / api_key / private_key / credential / client_secret / refresh_token has its value collapsed to the literal string `[REDACTED]`. Shape varies by action; agents should treat it as opaque unless they recognise the action.","additionalProperties":true}}},"RuleCondition":{"type":"object","description":"One leaf of a rule's condition tree. The evaluation engine short-circuits a rule when a `tier_2` condition is encountered without a Tier 2 delegation in place. `value` is intentionally loosely typed at the schema level — it is narrowed at evaluation time based on `operator` + `field`.","required":["field","operator","value","data_tier"],"properties":{"field":{"type":"string","description":"Dotted path to the field on the company context object the engine evaluates against (e.g. `entity_type`, `tier_2.annual_turnover`).","example":"tier_2.annual_turnover"},"operator":{"type":"string","enum":["eq","gt","gte","lt","lte","in","contains"],"description":"Comparison operator. Append-only vocabulary — the evaluation engine switches on this exhaustively."},"value":{"description":"Comparison value. Type depends on `operator` + `field`; narrowed at evaluation time."},"data_tier":{"type":"string","enum":["tier_1","tier_2"],"description":"Which data tier this condition reads from. `tier_2` conditions require an active Altinn delegation to evaluate."}}},"RuleOutcome":{"type":"object","description":"What fires when a rule's conditions evaluate true. A rule can emit multiple outcomes (e.g. a single condition that triggers both a filing obligation and a registration obligation).","required":["obligation_type","description","frequency","deadline_rule","legal_basis"],"properties":{"obligation_type":{"type":"string","description":"Canonical obligation identifier. Append-only vocabulary; the public obligations endpoint keys cache entries off these.","example":"mva_registration"},"description":{"type":"string","description":"Human-readable Norwegian description passed through to API consumers."},"frequency":{"type":"string","enum":["monthly","bimonthly","annual","one_time","continuous"],"description":"How often this obligation recurs. 'continuous' covers obligations that aren't deadline-driven (e.g. insurance that must be in force while employees are on payroll)."},"deadline_rule":{"type":"string","description":"Stable identifier for the deadline-calculation routine in src/lib/deadline. The evaluation engine looks this up and passes the relevant period to the deadline engine.","example":"mva_termin_next_business_day"},"legal_basis":{"type":"string","description":"Lovdata-style legal basis for the obligation.","example":"Merverdiavgiftsloven § 2-1"}}},"Rule":{"type":"object","description":"One row of the `rules` table. Rules are stored as structured data, not code (CLAUDE.md Rule 18), so the evaluation engine reads them at runtime and rule updates are database operations, not code deploys.","required":["rule_id","version","effective_date","expiry_date","legal_reference","source_url","conditions","outcomes","entity_types","is_active","last_verified","created_at"],"properties":{"rule_id":{"type":"string","description":"Human-readable stable id. Upper-snake by convention. Stable forever — renaming is a /v2 break (CLAUDE.md Rule 10).","example":"MVA_REGISTRATION_THRESHOLD"},"version":{"type":"integer","minimum":1,"description":"Monotonically increasing version per rule_id. A version bump is how a rule's content changes; the prior snapshot lives in rule_versions."},"effective_date":{"type":"string","format":"date-time","description":"ISO 8601 TIMESTAMPTZ when this rule version takes effect."},"expiry_date":{"type":"string","format":"date-time","nullable":true,"description":"ISO 8601 TIMESTAMPTZ when this rule version stops applying. Null means currently active with no scheduled sunset."},"legal_reference":{"type":"string","description":"Lovdata / official legal reference.","example":"Merverdiavgiftsloven § 2-1"},"source_url":{"type":"string","format":"uri","nullable":true,"description":"Link to Lovdata / Skatteetaten, or null when no stable public URL is published."},"conditions":{"type":"array","description":"Structured condition tree the evaluation engine walks against a company context.","items":{"$ref":"#/components/schemas/RuleCondition"}},"outcomes":{"type":"array","description":"Obligations that fire when conditions evaluate true.","items":{"$ref":"#/components/schemas/RuleOutcome"}},"entity_types":{"type":"array","description":"Norwegian entity-type codes this rule applies to. GIN-indexed for fast array-overlap lookup.","items":{"type":"string"},"example":["AS","ENK"]},"is_active":{"type":"boolean","description":"Soft-disable switch. The evaluation engine ignores `is_active=false` rows without touching the row (preserves version history)."},"last_verified":{"type":"string","format":"date-time","description":"ISO 8601 TIMESTAMPTZ when an operator last confirmed the rule against the source. Surfaces on `_meta.last_verified` for consumer trust."},"created_at":{"type":"string","format":"date-time","description":"ISO 8601 TIMESTAMPTZ when this rule version was first inserted."}}},"RuleVersion":{"type":"object","description":"One row of the `rule_versions` table (migration 010) — the append-only history. Append-only is enforced at three independent database layers (RLS policy set, BEFORE UPDATE OR DELETE trigger, revoked UPDATE/DELETE grants). `content` is deliberately loosely typed so older snapshots survive future rule-shape changes — consumers that want to validate should narrow at read time.","required":["id","rule_id","version","content","created_at"],"properties":{"id":{"type":"string","format":"uuid","description":"Row identifier."},"rule_id":{"type":"string","description":"Foreign key to rules.rule_id."},"version":{"type":"integer","minimum":1,"description":"The version snapshot this row captures. Unique per (rule_id, version)."},"content":{"description":"Full JSONB snapshot of the rule at this version. Shape may not match the current Rule schema if the rule model has evolved since this row was written."},"change_reason":{"type":"string","nullable":true,"description":"Optional operator-facing note for why this version was cut."},"created_at":{"type":"string","format":"date-time","description":"ISO 8601 TIMESTAMPTZ when this snapshot was inserted."}}},"EvaluationResult":{"type":"object","description":"One verdict from the Universal Rulebook evaluator. The evaluator is pure (CLAUDE.md Rule 22) and deterministic (Rule 9) — the same company context + same rule set produces the same ordered array. Results are always sorted by `rule_id` ASC. `evaluation_result` is `applicable` when every condition matched, `not_applicable` when a condition failed, and `insufficient_data` when a condition references a Tier 2 field that is missing on the context (agents should then surface a Tier 2 delegation path). Exposed via GET /api/v1/company/{org}/obligations.","required":["rule_id","obligation_name","description","frequency","legal_reference","data_tier_required","evaluation_result","reason","deadline_rule"],"properties":{"rule_id":{"type":"string","description":"Matches `rules.rule_id` in migration 010.","example":"MVA_REGISTRATION_THRESHOLD"},"obligation_name":{"type":"string","description":"Obligation name taken from the rule's first outcome. Convenience for UI / agent rendering.","example":"mva_registration"},"description":{"type":"string","description":"Norwegian description passed through from the rule outcome."},"frequency":{"type":"string","enum":["monthly","bimonthly","annual","one_time","continuous"],"description":"How often this obligation recurs. Aligned with RuleOutcomeFrequency."},"legal_reference":{"type":"string","description":"Lovdata-style legal reference from the rule row.","example":"Merverdiavgiftsloven § 2-1"},"data_tier_required":{"type":"string","enum":["tier_1","tier_2"],"description":"Highest tier any condition in this rule reads from. `tier_2` when ANY condition has `data_tier: tier_2`; `tier_1` otherwise. Lets agents pre-flight whether a delegation is required to re-evaluate."},"evaluation_result":{"type":"string","enum":["applicable","not_applicable","insufficient_data"],"description":"Verdict. `insufficient_data` means a Tier 2 field was null or missing — the agent should trigger the delegation flow rather than treat it as yes/no."},"reason":{"type":"string","description":"Human-readable explanation. English for `insufficient_data` (so agents in any locale can match on a stable substring), Norwegian where the outcome copy is Norwegian."},"deadline_rule":{"type":"string","description":"Stable identifier for the deadline-calculation routine. Consumed by the deadline engine to expand this obligation into concrete due dates. Empty string when the rule has no outcome or no deadline_rule.","example":"mva_termin_10th_second_month"}}},"DeadlineEntry":{"type":"object","description":"One per-period deadline produced by the deadline engine. The engine is pure (CLAUDE.md Rule 22) and deterministic (Rule 9); for the same (obligations, from_date, horizon) input it emits byte-identical output, sorted by due_at ASC (nulls last) then obligation_id ASC. Every ISO 8601 timestamp carries the Europe/Oslo offset that applies at the instant (+01:00 CET or +02:00 CEST), derived dynamically via date-fns-tz — never hardcoded. Exposed via GET /api/v1/company/{org}/deadlines.","required":["obligation_id","obligation_name","period_label","due_at","submission_window_opens","submission_window_closes","adjusted_for","original_due_at","filing_status","data_tier_required"],"properties":{"obligation_id":{"type":"string","description":"Matches the source EvaluationResult.rule_id (and therefore rules.rule_id in the DB).","example":"AMELDING_MONTHLY"},"obligation_name":{"type":"string","description":"Norwegian human-readable obligation name, passed through from the rule outcome."},"period_label":{"type":"string","description":"Period label whose shape depends on the rule kind — \"2026-01\" (monthly payroll), \"2026-Termin-1\" (MVA bi-monthly), \"Årstermin-2026\" (MVA annual), \"2026\" (annual), or \"ongoing\" (continuous).","example":"2026-01"},"due_at":{"type":"string","format":"date-time","nullable":true,"description":"Final (post-adjustment) deadline. Null only for continuous obligations."},"submission_window_opens":{"type":"string","format":"date-time","nullable":true,"description":"When the filing window opens. 30 days before due_at for monthly, first day of first covered period month for termin filings, 1 January for annual, null for continuous / one-time."},"submission_window_closes":{"type":"string","format":"date-time","nullable":true,"description":"Last moment a filing is accepted on normal rails — same instant as due_at for deadline-driven rules, null for continuous."},"adjusted_for":{"type":"string","enum":["weekend","holiday","none"],"description":"Why the legal deadline was shifted forward. \"none\" when the legal date was already a Norwegian business day."},"original_due_at":{"type":"string","format":"date-time","nullable":true,"description":"Legal deadline BEFORE weekend/holiday adjustment. Equals due_at when no shift happened; null for continuous obligations."},"filing_status":{"type":"string","enum":["upcoming","overdue","filed","unknown"],"description":"Filing state for this (obligation, period). Sourced from the optional filing_status_lookup callback. Resolution rules: (a) if a lookup is supplied, its return value wins — including \"filed\"; exceptions thrown by the lookup are swallowed to \"unknown\" so engine emission is resilient to downstream state; (b) if no lookup is supplied, dated obligations auto-derive \"upcoming\" vs \"overdue\" from due_at compared to from_date, and only continuous obligations (no calendar deadline) fall back to \"unknown\"."},"data_tier_required":{"type":"string","enum":["tier_1","tier_2"],"description":"Passed through from EvaluationResult — which tier of data was required to decide this obligation applied."}}},"HandoverAction":{"type":"object","description":"Human next-step pointer emitted when the agent cannot resolve an error on its own (AUTH_* — delegation or role missing in Altinn, SCOPE_MISSING — Maskinporten tildeling mangler, UNKNOWN — needs Apier triage). Surfaces on the standard error envelope via Explanation.handover. Produced by the compliance explainer — pure, deterministic, Norwegian-bokmål text.","required":["who","where","what","why"],"properties":{"who":{"type":"string","enum":["company_admin","accountant","altinn_user","apier_support"],"description":"Which human role must act. Closed vocabulary so agents can branch on it — do not surface raw prose here."},"where":{"type":"string","description":"Stable URL (preferred) or offline location identifier. Example: https://www.altinn.no/ui/profile.","example":"https://www.altinn.no/ui/profile"},"what":{"type":"string","description":"Imperative Norwegian-bokmål sentence describing the action to take."},"why":{"type":"string","description":"Short Norwegian explanation of why only a human can resolve it — keeps agents from looping on an unfixable error."}}},"Explanation":{"type":"object","description":"Enriched Norwegian-bokmål explanation of a machine-readable error code. Produced by the compliance explainer — pure, deterministic, same (code, context) always yields deeply equal output. All strings are Norwegian; placeholders like {role} / {org_number} are interpolated from ExplainerContext or replaced with a Norwegian fallback (\"ukjent rolle\") — never left as a literal token. Embedded inline on every standard error envelope.","required":["error_code","summary","why","fix_steps","relevant_link","legal_basis","handover"],"properties":{"error_code":{"type":"string","enum":["AUTH_INSUFFICIENT_ROLE","AUTH_NO_DELEGATION","VALIDATION_FAILED","SCOPE_MISSING","RATE_LIMIT_EXCEEDED","NOT_FOUND","UPSTREAM_UNAVAILABLE","IDEMPOTENCY_KEY_MISMATCH","IDEMPOTENCY_IN_PROGRESS","UNKNOWN"],"description":"Machine-readable code the explanation enriches. Closed vocabulary; agents should branch on this, not on the Norwegian strings. Mirrors the published API error codes (RATE_LIMIT_EXCEEDED in src/middleware/rate-limit.ts, IDEMPOTENCY_KEY_MISMATCH / IDEMPOTENCY_IN_PROGRESS in src/middleware/idempotency.ts)."},"summary":{"type":"string","maxLength":140,"description":"One-sentence Norwegian summary suitable for inline display.","example":"Systembrukeren mangler nødvendig Altinn-rolle for denne handlingen."},"why":{"type":"string","maxLength":240,"description":"Slightly longer Norwegian explanation of the root cause."},"fix_steps":{"type":"array","minItems":2,"maxItems":5,"items":{"type":"string"},"description":"Two to five concrete imperative Norwegian next steps."},"relevant_link":{"type":"string","format":"uri","nullable":true,"description":"Public Altinn / Skatteetaten / Brønnøysund / Apier-docs URL when one applies. Null otherwise."},"legal_basis":{"type":"string","nullable":true,"description":"Lovdata-style legal reference when the error maps to a specific regulatory provision. Null otherwise."},"handover":{"allOf":[{"$ref":"#/components/schemas/HandoverAction"}],"nullable":true,"description":"Human handover pointer when the agent cannot resolve the error itself (AUTH_INSUFFICIENT_ROLE, AUTH_NO_DELEGATION, SCOPE_MISSING, UNKNOWN). Null for agent-resolvable errors (VALIDATION_FAILED, RATE_LIMIT_EXCEEDED, NOT_FOUND, IDEMPOTENCY_KEY_MISMATCH, IDEMPOTENCY_IN_PROGRESS, UPSTREAM_UNAVAILABLE) — agents can retry, adjust input, or wait without human intervention."}}},"PublicDeadlineEntry":{"type":"object","description":"One entry in the /v1/public/deadlines response. Every date field uses ISO 8601 with an explicit Europe/Oslo offset (+01:00 CET or +02:00 CEST) derived dynamically via date-fns-tz — the offset applies to the ADJUSTED instant, not the pre-adjustment legal date.","required":["obligation_id","obligation_name","period","deadline","submission_window_closes","timezone","adjusted_from","legal_reference","applies_to_entity_types"],"properties":{"obligation_id":{"type":"string","description":"Stable machine-readable identifier. Suitable for hashing or caching. Example: `mva-termin-1-2026`.","example":"mva-termin-1-2026"},"obligation_name":{"type":"string","description":"Norwegian display name. Example: `MVA-melding, 1. termin 2026`.","example":"MVA-melding, 1. termin 2026"},"period":{"type":"string","description":"Human-readable period covered. Example: `Januar–februar 2026`.","example":"Januar–februar 2026"},"deadline":{"type":"string","format":"date-time","description":"Effective deadline (already adjusted forward for weekends + Norwegian public holidays). ISO 8601 with an explicit Oslo offset.","example":"2026-04-10T23:59:59+02:00"},"submission_window_closes":{"type":"string","format":"date-time","description":"Last moment Altinn accepts a submission. Equal to `deadline` until we have evidence that the upstream grace window differs.","example":"2026-04-10T23:59:59+02:00"},"timezone":{"type":"string","description":"Always `Europe/Oslo`. Agents that render in another zone should convert using this hint.","example":"Europe/Oslo"},"adjusted_from":{"type":"string","format":"date-time","nullable":true,"description":"If the legal deadline was moved forward by a weekend or holiday, the pre-adjustment legal deadline (also ISO 8601 with Oslo offset). `null` when the legal deadline was already a business day.","example":"2026-05-31T23:59:59+02:00"},"legal_reference":{"type":"string","description":"Lovdata-style legal reference the deadline derives from.","example":"Skatteforvaltningsloven § 8-3"},"applies_to_entity_types":{"type":"array","description":"Norwegian entity-type codes this deadline applies to. A subset of [AS, ENK, ANS, DA, NUF].","items":{"type":"string","example":"AS"}}}},"UniversalObligation":{"type":"object","description":"One entry in the /v1/public/obligations response — a generic template of a Norwegian business obligation for a given entity type. These are TEMPLATES, not per-company evaluations; the paid /v1/company/{org}/obligations endpoint evaluates the same list against Tier 2 data (employees, turnover, MVA status). Agents read `tier_2_required` and `condition` to plan delegation flows before reaching for the evaluated endpoint.","required":["obligation_id","obligation_name","category","frequency","required","condition","tier_2_required","legal_reference","source_url"],"properties":{"obligation_id":{"type":"string","description":"Stable machine-readable key. Append-only forever — renaming or removing one is a /v2 break.","example":"mva-melding"},"obligation_name":{"type":"string","description":"Norwegian display name.","example":"MVA-melding"},"category":{"type":"string","enum":["tax","reporting","insurance","registration"],"description":"Broad classification of the obligation."},"frequency":{"type":"string","enum":["one-time","monthly","bimonthly","quarterly","annual"],"description":"How often the obligation recurs."},"required":{"type":"string","enum":["always","conditionally","never"],"description":"Tri-state applicability. `never` entries are included deliberately — e.g. revisor-plikt for ENK — so agents see the obligation was considered and explicitly ruled out, rather than assuming it was forgotten."},"condition":{"type":"string","nullable":true,"description":"Norwegian explanation of the conditional trigger, present iff `required === \"conditionally\"` (and always present when `tier_2_required === true` so the agent knows the trigger it must evaluate).","example":"Påkrevd når samlet omsetning overstiger 50 000 NOK i en 12-måneders periode."},"tier_2_required":{"type":"boolean","description":"True when evaluating this obligation for a specific company requires Tier 2 commercial data (employee count, turnover, MVA registration status). Agents use this flag to plan a delegation flow before calling the evaluated /v1/company/{org}/obligations endpoint."},"legal_reference":{"type":"string","description":"Lovdata-style legal reference.","example":"Skatteforvaltningsloven § 8-3"},"source_url":{"type":"string","format":"uri","description":"Authoritative source link — Lovdata or Skatteetaten.","example":"https://lovdata.no/lov/2016-05-27-14/%C2%A78-3"}}},"MigrationEntry":{"type":"object","description":"One Altinn 2 → Altinn 3 lookup row. DigDir's published migration documentation is the authoritative source; this endpoint is a convenience only. Consumers MUST respect `verified`: `false` means the mapping has not been cross-checked against DigDir and should not drive an automated production migration without human review.","required":["altinn2_code","altinn2_name_nb","altinn3_code","altinn3_name_nb","service_owner","migration_notes_nb","migration_notes_en","verified"],"properties":{"altinn2_code":{"type":"string","description":"Altinn 2 service or role code (alphanumeric, up to 10 chars).","example":"A0208"},"altinn2_name_nb":{"type":"string","description":"Norwegian display name of the Altinn 2 service or role.","example":"Merverdiavgiftsmelding"},"altinn3_code":{"type":"string","nullable":true,"description":"Altinn 3 identifier (resource URN or role code). Null when DigDir has not yet published the Altinn 3 equivalent.","example":null},"altinn3_name_nb":{"type":"string","nullable":true,"description":"Norwegian display name for the Altinn 3 equivalent. Null when altinn3_code is null.","example":null},"service_owner":{"type":"string","description":"Service owner — Skatteetaten, Brønnøysundregistrene, NAV, etc. Empty string when not attributable.","example":"Skatteetaten"},"migration_notes_nb":{"type":"string","description":"Norwegian-language migration guidance."},"migration_notes_en":{"type":"string","description":"English-language mirror of `migration_notes_nb`."},"verified":{"type":"boolean","description":"True only when cross-checked against DigDir's official documentation. False means the entry is a best-effort reference and must be verified before any production workflow relies on it."}}},"CapabilityEntry":{"type":"object","description":"One entry in the `/api/v1/capabilities` manifest. APPEND-ONLY: `id` and `endpoint.path` are stable contracts; renaming either is a /v2 break. New optional fields may be added without version bumps.","required":["id","name_en","name_nb","description_en","description_nb","endpoint","auth","tier_minimum","category","openapi_operation_id","example_request","data_sources","freshness"],"properties":{"id":{"type":"string","description":"Stable dot-separated id (e.g. `public.deadlines`). Never rename.","example":"public.deadlines"},"name_en":{"type":"string","description":"Short English display name."},"name_nb":{"type":"string","description":"Short Norwegian display name."},"description_en":{"type":"string","description":"One-to-three sentence English description from the agent's perspective."},"description_nb":{"type":"string","description":"Norwegian mirror of `description_en`."},"endpoint":{"type":"object","required":["method","path"],"properties":{"method":{"type":"string","enum":["GET","POST","PUT","DELETE","PATCH"]},"path":{"type":"string","description":"Path with `{braces}` for parameters, aligned with the path keys in this OpenAPI document (i.e. `/api/v1/...`).","example":"/api/v1/public/deadlines"}}},"auth":{"type":"string","enum":["none","api_key","admin_api_key"],"description":"`none` = zero-auth. `api_key` = Bearer consumer API key required (tier-gated). `admin_api_key` = Bearer ADMIN_API_KEY operator secret; consumer keys cannot call these paths."},"tier_minimum":{"anyOf":[{"type":"null"},{"type":"string","enum":["free","starter","professional","enterprise"]}],"description":"Lowest consumer tier that may call this capability. Null when auth is `none` or when the route is admin-only."},"category":{"type":"string","enum":["public-tool","company-data","auth","admin","discovery"]},"openapi_operation_id":{"type":"string","description":"Matches the `operationId` elsewhere in this OpenAPI spec. Agents follow it to get full request/response shapes."},"example_request":{"type":"string","description":"Minimal curl or query-string example; placeholders for API keys and org numbers."},"data_sources":{"type":"array","description":"Free-form upstream/source tags (e.g. `brreg`, `norges-bank`, `digdir`, `internal-supabase`, `internal-static`).","items":{"type":"string"}},"freshness":{"type":"string","enum":["static","live","cached","on-demand"],"description":"How the underlying data is refreshed."}}},"ComparisonApproach":{"type":"object","description":"One approach in a structured Apier-vs-alternative comparison. Nulls on `success_rate` and `p95_latency_ms` are INTENTIONAL honesty markers — for Apier they ship as static placeholders until PR-D telemetry lands; for competing approaches they stay null because we don't instrument them. Agents can safely distinguish 'unknown' from 'zero'.","required":["success_rate","p95_latency_ms","failure_modes","retry_handling","error_normalization","deadline_intelligence","cross_agency_orchestration","complexity","auth_setup_steps"],"properties":{"success_rate":{"anyOf":[{"type":"null"},{"type":"number","minimum":0,"maximum":1}],"description":"Fraction 0.0–1.0. Null when the approach isn't instrumented by Apier."},"p95_latency_ms":{"anyOf":[{"type":"null"},{"type":"integer","minimum":0}],"description":"p95 latency in milliseconds. Null when not instrumented."},"failure_modes":{"type":"array","description":"Canonical failure-mode identifiers so an agent can plan mitigations. Append-only: renaming an entry is a /v2 break.","items":{"type":"string"}},"retry_handling":{"type":"string","enum":["automatic","none"],"description":"`automatic` when the approach retries transient failures on behalf of the caller; `none` when the caller must retry themselves."},"error_normalization":{"type":"boolean","description":"True when the approach surfaces a consistent error envelope across Altinn, Brønnøysund, Skatteetaten, and NAV."},"deadline_intelligence":{"type":"boolean","description":"True when the approach understands Norwegian regulatory deadlines rather than just proxying raw API calls."},"cross_agency_orchestration":{"type":"boolean","description":"True when one call can coordinate state across multiple Norwegian agencies."},"complexity":{"type":"string","enum":["low","medium","high","very_high"],"description":"Integration complexity from the caller's perspective."},"auth_setup_steps":{"type":"integer","minimum":0,"description":"Discrete integration steps required before the first production call."}}},"ComparisonResponse":{"type":"object","description":"Machine-readable head-to-head matrix for an agent choosing between Apier and a competing approach. Not marketing copy — designed to be parsed, not rendered.","required":["approaches","recommendation","last_updated"],"properties":{"approaches":{"type":"object","description":"Map of approach key (e.g. `apier`, `direct_altinn`, `scraping`) to a `ComparisonApproach`.","additionalProperties":{"$ref":"#/components/schemas/ComparisonApproach"}},"recommendation":{"type":"string","description":"Key into `approaches` naming the recommended choice.","example":"apier"},"last_updated":{"type":"string","format":"date-time","description":"ISO 8601 TIMESTAMPTZ of the last human review of the comparison data."}}},"ReceiptSignatureAlgorithm":{"type":"string","enum":["HMAC-SHA256-v1"],"description":"Versioned algorithm tag for signed submission receipts (PR-075, Amendment 59 Invariant 5). v1 today: HMAC-SHA256 over the canonical-JSON form of the receipt body (with the `signature` field excluded; `signature_algorithm` itself IS in the hashed domain — downgrade-attack defence). A future rotation introduces a new value (`HMAC-SHA256-v2` on secret rotation, `Ed25519-v1` on asymmetric migration); historical receipts continue to verify under the algorithm they were signed with because the verifier branches on this column at read time."},"LargeGovernmentResponse":{"type":"object","description":"Compact pointer the receipt row carries on `government_response_raw` when the canonicalised normalised payload exceeded 256KB and was spilled into the `large_government_responses` table (PR-075). The full bytes live one JOIN away on `large_government_responses.full_payload`; this sentinel is what a forensic peek at the receipt itself sees without joining. Stable shape regardless of upstream payload structure.","required":["truncated","byte_length","inline_projection_byte_budget","hash","note"],"additionalProperties":false,"properties":{"truncated":{"type":"boolean","enum":[true],"description":"Discriminator: always true. A non-truncated receipt carries the verbatim payload on `government_response_raw` instead of this sentinel."},"byte_length":{"type":"integer","minimum":262145,"description":"Byte length of the canonicalised normalised payload at signing time. Spill happens only when the canonical form is STRICTLY >256KB; exactly 256KB stays inline. Minimum 262145 (256KB + 1 byte) reflects this invariant — under-threshold payloads cannot legitimately appear in a `LargeGovernmentResponse`."},"inline_projection_byte_budget":{"type":"integer","minimum":1,"description":"Soft cap on the inline projection size (echoed for transparency). Today: 65536 (64 KB)."},"hash":{"type":"string","pattern":"^sha256:[0-9a-f]{64}$","description":"SHA-256 of canonicalJSON(normalised payload), prefixed `sha256:`. Joins to `large_government_responses.response_hash` and matches `receipts.government_response_hash`.","example":"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"},"note":{"type":"string","description":"Static guidance pointing at the spill-bucket join — instructive for a human reading the row in isolation."}}},"SignedReceipt":{"type":"object","description":"HMAC-signed government submission receipt (PR-075, Amendment 59 Invariant 5). Returned by `/api/v1/actions/execute` (PR-077) when an action submitted to Altinn / Skatteetaten / NAV produces a receipt. Persisted to the append-only `receipts` table so a future re-verify can confirm Apier issued exactly these bytes at exactly this signing time. Frame this as an integrity receipt, not a proof of legal correctness — Apier signs the wrapper, not the upstream's truth claim (CLAUDE.md Rule 8).\n\nThe `government_response_raw` field carries the upstream's response VERBATIM (minimally normalised: timestamps converted from Oslo-local to ISO 8601 with explicit Europe/Oslo offset, keys case-preserved, byte-level content otherwise preserved). Apier does not interpret this payload — it is preserved for defensibility. When the canonicalised form exceeds 256KB the field carries a `LargeGovernmentResponse` pointer instead, with the full bytes on `large_government_responses` joinable via the persisted `large_response_id` column (the join is server-side; the wire form is the same union shape).\n\nThe `signature` is HMAC-SHA256 over `canonicalJSON(receipt-without-signature)` using the operator-side `RECEIPT_HMAC_SECRET`. The hashed domain INCLUDES `signature_algorithm` — flipping the algorithm tag without recomputing the signature is a downgrade attack and fails verification.","required":["altinn_receipt_id","audit_log_id","org_number","filing_type","rule_version","correlation_id","government_response_raw","government_response_hash","signed_at","signature_algorithm","signature","normalization_rules_applied"],"additionalProperties":false,"properties":{"altinn_receipt_id":{"type":"string","minLength":1,"maxLength":256,"description":"Upstream-issued receipt id (Altinn / Skatteetaten / NAV). Forwarded verbatim from the gov API response."},"audit_log_id":{"type":"string","format":"uuid","description":"UUID of the `audit_log` row for the action that produced this receipt. Hard FK in the database; the receipt is always preceded by its audit row."},"org_number":{"type":"string","pattern":"^[0-9]{9}$","description":"9-digit Norwegian organisation number the receipt belongs to.","example":"987654321"},"filing_type":{"type":"string","minLength":1,"maxLength":128,"description":"Stable machine-readable filing-type identifier (e.g. `mva-melding`, `a-melding`, `skattemelding`). Free TEXT in v1; a future closed enum will be additive."},"rule_version":{"type":"string","minLength":1,"maxLength":64,"description":"Rulebook version active at signing time. Frozen on the receipt so a re-verify always reasons against the original rules — not whatever Rulebook version is current at re-verification."},"correlation_id":{"type":"string","format":"uuid","description":"Request-scope UUID v4. Joins `audit_log`, `provenance_log`, `evaluation_snapshots`, and `compliance_state_events` on the same forensic axis (Amendment 59 Invariant 1)."},"government_response_raw":{"description":"The exact JSON received from Altinn / Skatteetaten / NAV on submission. Minimally normalised: timestamps in Norwegian local-time form (`YYYY-MM-DD[ T]HH:mm:ss`) are converted to ISO 8601 with explicit Europe/Oslo offset (`+02:00` CEST / `+01:00` CET, DST-aware via date-fns-tz, never hardcoded). Keys are case-preserved (`MvaPeriode` stays `MvaPeriode`). All other byte-level value content is preserved. Apier does not interpret this payload — it is preserved verbatim for defensibility. When the canonicalised form is >256KB the receipt carries the spill-bucket `LargeGovernmentResponse` pointer here instead; consumers must inspect the shape via the `truncated` discriminator.\n\nThe inline branch accepts any JSON root (object, array, string, number, boolean, null) — Skatteetaten and Altinn endpoints occasionally return top-level arrays or primitives, and the lib treats `government_response_raw` as `unknown` (preserves verbatim). The branches use `anyOf` (not `oneOf`) because a `LargeGovernmentResponse` instance is also a generic object and would match BOTH branches, failing `oneOf`'s exactly-one constraint — the discriminator the consumer parses is `truncated`, not the JSON-Schema branch.","anyOf":[{"type":"object","additionalProperties":true},{"type":"array","items":{}},{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"},{"$ref":"#/components/schemas/LargeGovernmentResponse"}]},"government_response_hash":{"type":"string","pattern":"^sha256:[0-9a-f]{64}$","description":"SHA-256 of `canonicalJSON(normalised payload)`, prefixed `sha256:`. Computed against the FULL normalised payload (not the truncated projection) so re-verification can rehash from `large_government_responses.full_payload`.","example":"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"},"signed_at":{"type":"string","format":"date-time","description":"RFC 3339 / ISO 8601 timestamp of when the receipt was signed. Caller-supplied (PR-077 will pass the route's frozen time-source) so the signature is deterministic across retries."},"signature_algorithm":{"$ref":"#/components/schemas/ReceiptSignatureAlgorithm"},"signature":{"type":"string","pattern":"^[0-9a-f]{64}$","description":"HMAC-SHA256 hex (64 chars). Computed over `canonicalJSON(receipt-without-signature)` using the operator-side `RECEIPT_HMAC_SECRET`. INCLUDES `signature_algorithm` in the hashed domain — downgrade-attack defence."},"normalization_rules_applied":{"type":"array","description":"Diagnostic record of which `normalizeGovernmentResponse` rules fired during signing. Included in the HMAC domain (part of `canonicalJSON(receipt-without-signature)`) so an intermediary cannot rewrite normalization metadata silently — tampering with this list breaks signature verification (PR-075 round 1 Major). Echoed verbatim on the wire for forensic transparency.","items":{"type":"string","enum":["oslo_timestamp_normalised"]}}}},"MvaMeldingPayload":{"type":"object","description":"MVA-melding (Norwegian VAT return) payload — shape-only validation. Monetary fields are STRINGS, not numbers, because JS `number` is IEEE-754 double and silently loses precision on decimal money. Totals consistency (revenue >= outgoing + incoming VAT etc.) is NOT checked at this layer; PR-077's live-execute path adds semantic checks. PR-073.","required":["total_revenue_nok","total_outgoing_vat_nok","total_incoming_vat_nok"],"additionalProperties":false,"properties":{"total_revenue_nok":{"type":"string","pattern":"^\\d+(\\.\\d{1,2})?$","description":"Non-negative decimal NOK with optional 1–2 fractional digits. The regex permits zero — clients sending nil-revenue MVA-melding (rare but valid for dormant companies) need it."},"total_outgoing_vat_nok":{"type":"string","pattern":"^\\d+(\\.\\d{1,2})?$","description":"Non-negative decimal NOK."},"total_incoming_vat_nok":{"type":"string","pattern":"^\\d+(\\.\\d{1,2})?$","description":"Non-negative decimal NOK."},"exchange_rate_used":{"type":"object","description":"Optional currency-conversion snapshot when invoices reference non-NOK transactions. When present, both fields are required.","required":["currency","rate"],"additionalProperties":false,"properties":{"currency":{"type":"string","pattern":"^[A-Z]{3}$","description":"ISO 4217 currency code (3 uppercase letters)."},"rate":{"type":"string","pattern":"^\\d+(\\.\\d{1,4})?$","description":"Non-negative decimal exchange rate with optional 1–4 fractional digits. Norges Bank publishes rates at 4-decimal precision (e.g., USD/NOK ~10.1234), so the rate field accepts up to 4 fractional digits — distinct from the 1–2-decimal NOK monetary fields elsewhere in this schema."}}}}},"AMeldingPayload":{"type":"object","description":"A-melding (Norwegian employer report) payload — shape-only validation. fnr_or_dnr is FORMAT-only (11 digits); mod-11 is NOT validated at this layer. Apier MUST NOT echo or mod-11-validate fødselsnummer (data minimisation, GDPR Art 5). Employee array bounded 1–2000 to stay within the 256 KB request-body transport cap. PR-073.\n\nThe `reporting_period` field is duplicated with the request body's top-level `period` field (the two fields name the same calendar month). The route boundary's Zod superRefine enforces equality — submitting `period: \"2026-04\"` with `payload.reporting_period: \"2026-05\"` returns 400 VALIDATION_FAILED. The duplication exists for SDK ergonomics: generators that bind `period` to the URL form and `reporting_period` to the body shape stay simple; agents MUST keep the two in sync.\n\nEffective upper bound is the 256 KB request body limit. The schema cap of 2000 employees is the size at which a typical payload (~110 bytes per employee) approaches the transport ceiling; submissions over the body limit return 413 PAYLOAD_TOO_LARGE regardless of array length. PR-073 round 21 (CodeRabbit Major) lowered the cap from 5000 to 2000 to keep the documented schema transport-reachable — generated clients no longer accept requests the server can only answer with 413.","required":["reporting_period","employees"],"additionalProperties":false,"properties":{"reporting_period":{"type":"string","pattern":"^\\d{4}-(0[1-9]|1[0-2])$","description":"Calendar month — `YYYY-MM`. MUST equal the request body's top-level `period` field — enforced by the route boundary."},"employees":{"type":"array","minItems":1,"maxItems":2000,"items":{"type":"object","required":["fnr_or_dnr","gross_salary_nok","hours_worked"],"additionalProperties":false,"properties":{"fnr_or_dnr":{"type":"string","pattern":"^\\d{11}$","maxLength":11,"description":"Exactly 11 digits — Norwegian fnr/d-number FORMAT only. Mod-11 not validated here. `maxLength` is defence-in-depth alongside the regex (PR-073 round 21)."},"gross_salary_nok":{"type":"string","pattern":"^\\d+(\\.\\d{1,2})?$","maxLength":16,"description":"Non-negative decimal NOK gross salary for the period. `maxLength` 16 covers values up to 9999999999.99 NOK with headroom; the regex constrains the shape further."},"hours_worked":{"type":"string","pattern":"^\\d+(\\.\\d{1,2})?$","maxLength":10,"description":"Non-negative decimal hours worked for the period. `maxLength` 10 covers values up to 9999999.99 hours; realistic month-bucket values are ~160."}}}}}},"DryRunCheckResult":{"type":"object","description":"One row of the `DryRunOutcome.checks` array. Every check produces exactly one of these. `error_code` and `explanation_summary` are null on a passing check. PII boundary: `explanation_summary` lists FIELD PATHS only on validation failures, never field values. The optional `details` field carries check-specific structured remediation data (PR-073 round 15) — today only the SCOPE_INSUFFICIENT_FOR_ACTION branch of `scopes_delegated` populates it; other branches omit the field. Append-only per Rule 10 — `additionalProperties` is intentionally OPEN so future optional response fields can be added without breaking strict-validating clients (PR-073 round 16, CodeRabbit Major). PR-073.","required":["check","passed","error_code","explanation_summary"],"properties":{"check":{"type":"string","enum":["company_exists","system_user_authorised","scopes_delegated","data_format_valid","deadline_in_future"],"description":"Closed enum of dry-run check names."},"passed":{"type":"boolean","description":"True iff this individual check passed."},"error_code":{"type":["string","null"],"description":"Machine-readable error code when `passed` is false; null otherwise. Common values: COMPANY_NOT_FOUND, AUTH_NO_DELEGATION, SCOPE_INSUFFICIENT_FOR_ACTION, VALIDATION_FAILED, DEADLINE_PASSED, COMPANY_LOOKUP_FAILED, AUTH_LOOKUP_FAILED, DEADLINE_LOOKUP_FAILED. Reserved (no longer emitted as of PR-073 round 20): VALIDATION_PRECONDITION_FAILED — the engine previously emitted this on the deadline check when data_format_valid had failed, but round 20 reverted the short-circuit because the deadline lookup uses only org_number / action_type / period (all validated at the route boundary). The code stays in the documented enum so any client that pattern-matched on it continues to typecheck (Rule 10 — append-only response contract)."},"explanation_summary":{"type":["string","null"],"description":"One-line human-readable summary of why this check failed. Null on every passing check EXCEPT the `deadline_in_future` passing-but-noteworthy carveout (the `getDeadline` adapter returned null for the action+period combination, so the check passes with a non-null summary noting the unconstrained state — see `runDeadlineCheck` in src/lib/actions/dry-run.ts). PR-073 round 19 (CodeRabbit Minor) — explicit carveout documented to disambiguate the prior wording. For all four other checks: `passed: true` ⇒ `explanation_summary: null`; `passed: false` ⇒ `explanation_summary: <string>`. Norwegian explainer details live on the explainer surface. Field paths only on validation — never values. PR-073 round 15: the SCOPE_INSUFFICIENT_FOR_ACTION branch emits a STATIC summary (round-4 consistency); the per-call data lives on `details` instead."},"details":{"type":"object","description":"Optional check-specific structured data. Append-only per Rule 10 — `additionalProperties` is intentionally OPEN so future per-check shapes can be added without breaking strict-validating clients (PR-073 round 16). Today two check branches populate this: `scopes_delegated` → SCOPE_INSUFFICIENT_FOR_ACTION emits `action_type` + `missing_scopes` so consumers can programmatically remediate a delegation without parsing `explanation_summary`; `data_format_valid` → VALIDATION_FAILED emits `zod_issues` (redacted Zod issue records — path + code + message, with quoted received-value substrings stripped per Rule 24) so SDK clients can surface the exact field path that failed without inventing their own parser. Absent on passing checks and on most failure branches. PR-073 round 22 (CodeRabbit Major): `zod_issues` documented here so the OpenAPI surface matches the runtime contract from round 18.","properties":{"action_type":{"type":"string","enum":["mva_melding","a_melding"],"description":"The action_type the check was evaluated against. Mirrors the request body's action_type field. Populated on `scopes_delegated` → SCOPE_INSUFFICIENT_FOR_ACTION."},"missing_scopes":{"type":"array","items":{"type":"string"},"description":"Maskinporten gov-API scopes the consumer's System User delegation is missing for the given action_type. Public identifiers — safe to surface programmatically. EXACT match required at the upstream token level (no wildcard semantics, unlike Apier API-key scopes per Rule 32). Populated on `scopes_delegated` → SCOPE_INSUFFICIENT_FOR_ACTION."},"zod_issues":{"type":"array","description":"Redacted Zod issue records from the per-action payload safeParse at the route boundary (PR-073 round 18 — Zod runs at boundary; engine consumes the SafeParseResult). Each entry pinpoints the field that failed shape validation. Populated on `data_format_valid` → VALIDATION_FAILED only. The `message` field is auto-redacted of any double- or single-quoted received-value substrings (Rule 24 — no internal leakage; Zod's default templating like `Expected number, received string \"abc\"` gets the value replaced with `<redacted>`) and truncated to 200 chars. The `path` field is the SCHEMA path (e.g. `payload.employees[0].fnr_or_dnr`), not the data — schema paths are NOT PII. The `code` field is Zod's enum tag (`invalid_type`, `too_small`, `unrecognized_keys`, etc.).","items":{"type":"object","required":["path","code","message"],"properties":{"path":{"type":"string","description":"Dot-plus-bracket field path. `(root)` when the issue applies at the top level."},"code":{"type":"string","description":"Zod issue code (e.g. `invalid_type`, `too_small`, `unrecognized_keys`)."},"message":{"type":"string","description":"Human-readable issue message with received-value substrings redacted; truncated to 200 chars."}}}}}}}},"DryRunExecuteMvaRequest":{"type":"object","description":"Request body for `POST /api/v1/actions/execute?dry_run=true` with `action_type: \"mva_melding\"`. Discriminated branch — referenced from the operation's `oneOf` request body schema. PR-073 round 2: extracted as a named component so the discriminator mapping points at a schema that ACTUALLY contains the `action_type` discriminator property (CodeRabbit + Cursor BugBot Medium — round 1 mapping pointed at `MvaMeldingPayload` which is payload-only and lacks the discriminator). PR-073 round 17 (CodeRabbit Major): `payload` is intentionally permissive (`type: object`) at the transport layer so a malformed payload reaches the dry-run engine and emerges as `data_format_valid: false` in the 200 response — the engine's per-action shape validation IS one of the 5 reportable checks by contract; gating it at the OpenAPI request-body schema would force generated clients to short-circuit those failures into 400s and break the all-checks-reported promise. The expected payload shape is documented separately at `#/components/schemas/MvaMeldingPayload` for SDK type generation.","required":["org_number","action_type","period","payload"],"additionalProperties":false,"properties":{"org_number":{"type":"string","pattern":"^\\d{9}$","description":"9-digit Norwegian organisation number."},"action_type":{"type":"string","const":"mva_melding","description":"MVA-melding (Norwegian VAT return)."},"period":{"type":"string","pattern":"^(\\d{4}-T[1-6])$|^(\\d{4}-A)$|^(\\d{4}-(0[1-9]|1[0-2]))$","description":"MVA period: `YYYY-Tn` (n=1..6 bimonthly terminer) | `YYYY-A` (annual) | `YYYY-MM` (monthly)."},"payload":{"type":"object","description":"MVA-melding payload. The dry-run engine's `data_format_valid` check (one of the 5 reportable checks) evaluates per-action shape conformance against `#/components/schemas/MvaMeldingPayload`. Transport-level validation only requires `type: object` so the engine can run all 5 checks regardless of payload validity — a malformed payload returns 200 with `outcome.checks[?check=='data_format_valid'].passed === false`, NOT 400. PR-073 round 17 (CodeRabbit Major)."}}},"DryRunExecuteAMeldingRequest":{"type":"object","description":"Request body for `POST /api/v1/actions/execute?dry_run=true` with `action_type: \"a_melding\"`. Discriminated branch — referenced from the operation's `oneOf` request body schema. PR-073 round 2. PR-073 round 17 (CodeRabbit Major): `payload` is intentionally permissive (`type: object`) at the transport layer so a malformed payload reaches the dry-run engine and emerges as `data_format_valid: false` in the 200 response — see `DryRunExecuteMvaRequest.payload` for the full rationale.","required":["org_number","action_type","period","payload"],"additionalProperties":false,"properties":{"org_number":{"type":"string","pattern":"^\\d{9}$","description":"9-digit Norwegian organisation number."},"action_type":{"type":"string","const":"a_melding","description":"A-melding (Norwegian employer report)."},"period":{"type":"string","pattern":"^\\d{4}-(0[1-9]|1[0-2])$","description":"A-melding period: `YYYY-MM` calendar month."},"payload":{"type":"object","description":"A-melding payload. The dry-run engine's `data_format_valid` check evaluates per-action shape conformance against `#/components/schemas/AMeldingPayload`. Transport-level validation only requires `type: object` so the engine can run all 5 checks regardless of payload validity — a malformed payload returns 200 with `outcome.checks[?check=='data_format_valid'].passed === false`, NOT 400. PR-073 round 17 (CodeRabbit Major)."}}},"DryRunOutcome":{"type":"object","description":"Top-level dry-run outcome returned to the caller. The `disclaimer` carries the agent contract: a passing dry-run is NOT a guarantee of submission success — government systems may reject otherwise-valid submissions for reasons outside Apier's view. Append-only per Rule 10 — `additionalProperties` is intentionally OPEN so future top-level outcome fields can be added without breaking strict-validating clients (PR-073 round 16, CodeRabbit Major). PR-073.","required":["all_passed","checks","disclaimer"],"properties":{"all_passed":{"type":"boolean","description":"True iff every check in `checks[]` passed."},"checks":{"type":"array","minItems":5,"maxItems":5,"description":"EXACTLY five entries at v1, in the engine's evaluation order: company_exists, system_user_authorised, scopes_delegated, data_format_valid, deadline_in_future. Order-stable across releases (Rule 10). The fixed-length-tuple invariant mirrors `src/lib/actions/types.ts` — generated clients can rely on positional access.","items":{"$ref":"#/components/schemas/DryRunCheckResult"},"prefixItems":[{"allOf":[{"$ref":"#/components/schemas/DryRunCheckResult"},{"properties":{"check":{"const":"company_exists"}}}]},{"allOf":[{"$ref":"#/components/schemas/DryRunCheckResult"},{"properties":{"check":{"const":"system_user_authorised"}}}]},{"allOf":[{"$ref":"#/components/schemas/DryRunCheckResult"},{"properties":{"check":{"const":"scopes_delegated"}}}]},{"allOf":[{"$ref":"#/components/schemas/DryRunCheckResult"},{"properties":{"check":{"const":"data_format_valid"}}}]},{"allOf":[{"$ref":"#/components/schemas/DryRunCheckResult"},{"properties":{"check":{"const":"deadline_in_future"}}}]}]},"disclaimer":{"type":"string","description":"Fixed-string contract reminder. The exact value is locked in src/lib/actions/dry-run.ts; mirrored in /llms-full.txt."}}},"DryRunSuccessData":{"type":"object","description":"PR-077 round 5 — extracted from the inline 200 oneOf so the response can carry an explicit OpenAPI discriminator (CodeRabbit Major). The `dry_run: true` literal is the discriminator value SDK generators map to this branch.","required":["dry_run","outcome"],"properties":{"dry_run":{"type":"boolean","enum":[true],"description":"Discriminator — confirms the request was processed via the dry-run path."},"outcome":{"$ref":"#/components/schemas/DryRunOutcome"}}},"ExecuteSuccessData":{"type":"object","description":"PR-077 round 5 — extracted from the inline 200 oneOf so the response can carry an explicit OpenAPI discriminator (CodeRabbit Major). The `dry_run: false` literal is the discriminator value SDK generators map to this branch. Returned on a successful live-execute (PR-077). The signed receipt is the integrity record — `signed_receipt.signature` is HMAC-SHA256 over `canonicalJSON(receipt-without-signature)` and includes `signature_algorithm` in the hashed domain (downgrade-tag-flip fails verification). PR-A appends `execution_guarantee` + `outcome` to the success envelope.","required":["dry_run","altinn_receipt_id","receipt_id","submitted_at","signed_receipt","execution_guarantee","outcome"],"properties":{"dry_run":{"type":"boolean","enum":[false],"description":"Discriminator — confirms the request was processed via the live-execute path."},"altinn_receipt_id":{"type":"string","minLength":1,"description":"Upstream Altinn 3 instance id. Mock mode emits `MOCK-ALT-<16 lowercase hex>`; live mode emits the real Altinn-issued id."},"receipt_id":{"type":"string","format":"uuid","description":"Apier-side `receipts` table primary key — the persisted signed receipt's row UUID."},"submitted_at":{"type":"string","format":"date-time","description":"RFC 3339 timestamp the upstream accepted the submission. Same value as `signed_receipt.signed_at` (lockstep — see receipts/sign.ts module header)."},"signed_receipt":{"$ref":"#/components/schemas/SignedReceipt"},"execution_guarantee":{"$ref":"#/components/schemas/ExecutionGuarantee"},"outcome":{"$ref":"#/components/schemas/Outcome"}}},"ExecutionGuarantee":{"type":"object","description":"PR-A — per-call execution provenance.\n\n**Where this field appears (round 4 CodeRabbit clarification, round 7 enum-name correction, iter-29 sandbox extension, PR-AGENT-CONFLICT-LOCK round-3 extension):**\n- **Production** `POST /api/v1/actions/execute` 200 — live-execute branch only (`data.dry_run === false`). The dry-run branch (`data.dry_run === true`) does NOT carry this field because dry-run skips the orchestrator entirely on the production surface. Post-orchestrator (orchestrator ran, `upstream_healthy` is the authoritative breaker snapshot).\n- **Production** `POST /api/v1/actions/execute` 502 — `GOVERNMENT_API_ERROR` and the other apier_codes the orchestrator preserves (`EXECUTION_CIRCUIT_OPEN`, `EXECUTION_DEADLINE_EXCEEDED`, `RETRY_BUDGET_EXHAUSTED`, `MASKINPORTEN_AUTH_FAILED`, `ALTINN_DELEGATION_MISSING`, `GOVERNMENT_RATE_LIMITED`, `GOVERNMENT_UNAVAILABLE`, `GOVERNMENT_VALIDATION_REJECTED`, `EXECUTION_FAILED` (round 29 — post-submit local-failure path: Altinn submit succeeded but signReceipt/persistReceipt threw)). The 10-value subset is exposed as `ApierPostOrchestratorErrorCode`. Post-orchestrator.\n- **Production** `POST /api/v1/actions/execute` 409 `CONCURRENT_WRITE_IN_FLIGHT` AND 503 `SAFETY_SYSTEM_UNAVAILABLE` — Pre-orchestrator (the conflict-lock gate fires BEFORE consumeApprovalToken / orchestrator). The route synthesizes `execution_guarantee` via `preOrchestratorErrorResponse` so generated SDKs see a uniform shape across status codes. The `upstream_healthy: true` + `circuit_state: \"CLOSED\"` values are SYNTHETIC defaults — a write conflict / lock-RPC outage tells us nothing about gov-API health, so the breaker snapshot is non-authoritative on these paths. The synthesized values keep the Conflict409Body / SafetyUnavailable503Body schema shape consistent with the post-orchestrator paths; the `X-Apier-Upstream-Healthy` HTTP header is INTENTIONALLY OMITTED on these branches because the spec convention reserves the header for authoritative breaker snapshots only.\n- **Production** `POST /api/v1/auth/system-user/delegate` 409 `CONCURRENT_WRITE_IN_FLIGHT` AND 503 `SAFETY_SYSTEM_UNAVAILABLE` — same pre-orchestrator synthetic shape (PR-AGENT-CONFLICT-LOCK).\n- **Sandbox** `POST /api/v1/sandbox/actions/execute` — EVERY response (200 success including dry_run, 4xx/5xx errors). PR-074-sandbox-execute applies the universal-emission contract on the sandbox mirror so SDK parsers don't have to branch on shape. Sandbox emits fixed PR-A values (HAPPY_PATH_* on success, DENIED_* on errors); the orchestrator is NOT invoked because sandbox is deterministic.\n\n**Where this field does NOT appear:** Production dry-run responses, production pre-orchestrator validation/scope/idempotency-key/body-bound rejections that do NOT route through `preOrchestratorErrorResponse`, the production `/v1/actions/plan` endpoint (does not exist today), the sandbox approval-token + plan endpoints (which are not action-execution paths), and any other endpoint outside `/v1/actions/execute`, `/v1/auth/system-user/delegate`, and `/v1/sandbox/actions/execute`.\n\nTells the SDK client whether retries fired, whether the upstream answered cleanly, and how long the call took end-to-end (including retry sleeps). Mirrors `ExecutionGuarantee` in src/lib/reliability/types.ts. Append-only per Rule 10 on the responses where it appears.","required":["retried","retries_remaining","normalized","upstream_healthy","circuit_state","upstream_system","execution_time_ms"],"properties":{"retried":{"type":"integer","minimum":0,"description":"How many retries actually fired (0 = first attempt succeeded)."},"retries_remaining":{"type":"integer","minimum":0,"description":"Retries left in the budget when the call returned."},"normalized":{"type":"boolean","description":"True iff the response went through the error normalizer (apier_code is the stable code mapped from raw upstream)."},"upstream_healthy":{"type":"boolean","description":"Snapshot of the circuit-breaker verdict at response time. Boolean projection of `circuit_state` (true iff CLOSED). Typically mirrored on the X-Apier-Upstream-Healthy header for authoritative post-orchestrator responses. Pre-orchestrator synthetic responses (emitted via `preOrchestratorErrorResponse`, e.g., `Conflict409Body` and `SafetyUnavailable503Body`) carry this field for client convenience but do NOT emit the header — the header is reserved for breaker snapshots taken AFTER the orchestrator runs."},"circuit_state":{"type":"string","enum":["CLOSED","OPEN","HALF_OPEN"],"description":"PR-A round 2 — full three-state circuit-breaker snapshot at response time. Distinct from upstream_healthy (boolean projection); preserves the HALF_OPEN probe state which the boolean form would collapse to OPEN. Outcome builder reads this to apply the +0.05 HALF_OPEN probability boost."},"upstream_system":{"$ref":"#/components/schemas/Upstream","description":"Which upstream gov system the orchestrator wrapped. Always present on every ExecutionGuarantee at runtime — all four orchestrator branches (CIRCUIT_OPEN synthetic, DEADLINE_EXCEEDED synthetic, success, normalized failure) and `synthesizeNoOrchestratorGuarantee` set this field. Listed in `required[]` deliberately (round 7+9 CodeRabbit re-flagged its omission; declined both rounds — the runtime contract is honest)."},"execution_time_ms":{"type":"integer","minimum":0,"description":"Wall-clock ms from orchestrator entry to return; includes retry sleeps + jitter."}}},"Outcome":{"type":"object","description":"PR-A — calibrated agent-facing outcome estimate. Included on the SAME response paths as `ExecutionGuarantee` — see `ExecutionGuarantee.description` for the exact emit-site list. Iteration-29 polish (CodeRabbit Minor): description updated alongside `ExecutionGuarantee` to surface the sandbox-execute extension. PR-AGENT-CONFLICT-LOCK round-3: also emitted on the production 409 `CONCURRENT_WRITE_IN_FLIGHT` and 503 `SAFETY_SYSTEM_UNAVAILABLE` paths via `preOrchestratorErrorResponse` (synthetic values: `is_final: false`, `requires_followup: true`, `followup_action: \"retry\"`, `success_probability: 0.2`, `confidence: \"low\"` — the calibration is non-authoritative on these paths because the orchestrator did not run, but the field is present so SDK consumers see a uniform shape across status codes). Sandbox: every response on `POST /api/v1/sandbox/actions/execute` including dry-run and error paths, per PR-074's universal-emission contract — sandbox emits fixed values (HAPPY_PATH_* on success, DENIED_* on errors).\n\nNorwegian disclaimer (DO NOT EDIT — bokmål review queue locked under DECISIONS.md \"PR-A Pre-Launch Native Speaker Review Queue\"): \"Estimat basert på historiske data, dry-run resultat og oppstrømssystemets helse. Dette er IKKE en garanti for at handlingen vil lykkes. Estimatet kan endres uten varsel når kalibreringen forbedres med mer data.\"\n\nEnglish: \"Estimate based on historical data, dry-run result, and upstream system health. This is NOT a guarantee that the action will succeed. The estimate may change without notice as calibration improves with more data.\"","required":["success_probability","confidence","is_final","requires_followup"],"properties":{"success_probability":{"type":"number","minimum":0,"maximum":1,"description":"Calibrated probability the action will ultimately succeed. Rounded to 2 decimals. NEVER NaN — clamped + validated."},"confidence":{"type":"string","enum":["low","medium","high"],"description":"Confidence band on the probability estimate. 'low' when sample count < 30; 'high' when probability > 0.85 with sufficient samples; 'medium' otherwise."},"is_final":{"type":"boolean","description":"True when this response represents a terminal state (no follow-up call would change the outcome)."},"requires_followup":{"type":"boolean","description":"True when the agent should make a follow-up call (poll for receipt, mint new token, etc.)."},"followup_action":{"type":"string","description":"Optional machine-readable hint for the follow-up. Only present when requires_followup is true."}}},"Upstream":{"type":"string","enum":["altinn","skatteetaten","maskinporten","brreg","norges_bank","nav"],"description":"PR-A — closed enum of upstream gov systems the reliability layer wraps. Mirrors `UPSTREAMS` in src/lib/reliability/types.ts."},"ApierErrorCode":{"type":"string","description":"PR-A — Apier-stable error codes returned by the reliability layer. Decoupled from upstream raw codes via the error-mappings table. Append-only per Rule 10. Note: not every code in this enum is emitted on every endpoint — see `ApierPostOrchestratorErrorCode` for the narrower subset the orchestrator emits on its 502 failure path.","enum":["APPROVAL_TOKEN_REQUIRED","APPROVAL_TOKEN_INVALID","APPROVAL_TOKEN_USED","APPROVAL_TOKEN_EXPIRED","APPROVAL_TOKEN_MISMATCH","IDEMPOTENCY_KEY_REQUIRED","PLAN_INSUFFICIENT","EXECUTION_FAILED","EXECUTION_TIMEOUT","GOVERNMENT_API_ERROR","REQUEST_TOO_LARGE","GOVERNMENT_RATE_LIMITED","GOVERNMENT_UNAVAILABLE","GOVERNMENT_VALIDATION_REJECTED","MASKINPORTEN_AUTH_FAILED","ALTINN_DELEGATION_MISSING","EXECUTION_DEADLINE_EXCEEDED","EXECUTION_CIRCUIT_OPEN","RETRY_BUDGET_EXHAUSTED","FOLLOWUP_REQUIRED"]},"ApierPostOrchestratorErrorCode":{"type":"string","description":"PR-A round 6 (CodeRabbit) — narrowed subset of `ApierErrorCode` that the orchestrator emits on the 502 path. Excludes pre-orchestrator codes (APPROVAL_TOKEN_*, IDEMPOTENCY_KEY_REQUIRED, PLAN_INSUFFICIENT, REQUEST_TOO_LARGE) which never surface on the post-orchestrator failure branch. SDK clients consuming the 502 response can switch on a closed 10-value enum instead of the full 20-value parent enum. Round 30 (Cursor BugBot Medium) — added `EXECUTION_FAILED` for the post-submit-local-failure path (round 29: Altinn submit succeeded but signReceipt/persistReceipt threw afterward; route emits this code so SDK clients can route to reconcile-don't-retry guidance instead of a generic upstream-retry path).","enum":["GOVERNMENT_API_ERROR","GOVERNMENT_RATE_LIMITED","GOVERNMENT_UNAVAILABLE","GOVERNMENT_VALIDATION_REJECTED","MASKINPORTEN_AUTH_FAILED","ALTINN_DELEGATION_MISSING","EXECUTION_DEADLINE_EXCEEDED","EXECUTION_CIRCUIT_OPEN","RETRY_BUDGET_EXHAUSTED","EXECUTION_FAILED"]},"IdempotencyMismatchError":{"description":"PR-077 round 6 — discriminated 422 variant for the IDEMPOTENCY_KEY_MISMATCH case (same Idempotency-Key + different body — the original response is preserved; the wrapper rejects the conflict). Round 9 (CodeRabbit Minor) — base shifted from `ApiError` to `ApiErrorWithStrictProvenance` so generated clients see the SAME `_meta.response_timestamp` + `_meta.response_hash` REQUIRED contract that the shared `#/components/responses/IdempotencyMismatch` response advertises across every other endpoint. Without the strict-provenance base, this 422 branch would silently drop the provenance fields from the typed envelope, leaving SDK clients with optional fields where the runtime always emits them. Only the `error_code` is narrowed to a closed single-value enum so SDK generators emit a discriminating literal type for the 422 oneOf.","allOf":[{"$ref":"#/components/schemas/ApiErrorWithStrictProvenance"},{"type":"object","properties":{"error_code":{"type":"string","enum":["IDEMPOTENCY_KEY_MISMATCH"],"description":"Always `IDEMPOTENCY_KEY_MISMATCH` for this branch. The discriminator value at the 422 union level."}}}]},"ExecutePreconditionFailedError":{"description":"PR-077 round 5 — discriminated 422 variant for the live-execute precondition gate. Distinct from the IDEMPOTENCY_KEY_MISMATCH 422 case (handled by `IdempotencyMismatchError`). When live preconditions fail (one or more of the five dry-run checks reports passed=false), the route returns this envelope with `error_code: VALIDATION_FAILED` and `details` listing the failed check names. The approval token is NOT consumed on this path; the agent can fix inputs and retry with the same token.\n\nRound 9 follow-up — base lifted from `ApiError` to `ApiErrorWithStrictProvenance` so the 422 oneOf's two branches share an identical envelope shape (response_timestamp + response_hash + schema_version REQUIRED on `_meta`). Generated SDK clients consuming the 422 union see consistent _meta typing regardless of which branch they discriminate to. The narrowing overlay (error_code pinned to VALIDATION_FAILED + explanation.details typed as the failed-check tuple) is unchanged.","allOf":[{"$ref":"#/components/schemas/ApiErrorWithStrictProvenance"},{"type":"object","properties":{"error_code":{"type":"string","enum":["VALIDATION_FAILED"],"description":"Always `VALIDATION_FAILED` for the precondition-gate variant. Narrows ApiError.error_code's open string to a closed single-value enum so SDK generators emit a discriminating literal type."},"explanation":{"type":"object","required":["summary","details"],"properties":{"details":{"type":"array","minItems":1,"description":"One entry per failed precondition check. `field` is the check name (one of company_exists / system_user_authorised / scopes_delegated / data_format_valid / deadline_in_future per src/lib/actions/types.ts DryRunCheckName). `message` carries the static reason string; finer-grained per-check evidence is on the dry-run path's 200 response where the same engine ran. Rule 10 — append-only across new checks.","items":{"type":"object","required":["field","message"],"properties":{"field":{"type":"string","enum":["company_exists","system_user_authorised","scopes_delegated","data_format_valid","deadline_in_future"]},"message":{"type":"string"}}}}}}}}]},"ExecuteReliabilityError":{"description":"PR-A round 5 — typed 502 error envelope for `POST /api/v1/actions/execute` post-orchestrator failures. Extends `ApiErrorWithStrictProvenance` with `execution_guarantee` + `outcome` fields so SDK clients see the same reliability metadata on the failure path as on the success path. Round 6 — `error_code` narrowed from `ApierErrorCode` (full 20-value enum) to `ApierPostOrchestratorErrorCode` (10-value subset the orchestrator actually emits). The narrowing improves switch-statement exhaustiveness in generated SDKs (no need to handle pre-orchestrator codes that can't surface here).","allOf":[{"$ref":"#/components/schemas/ApiErrorWithStrictProvenance"},{"type":"object","required":["execution_guarantee","outcome"],"properties":{"error_code":{"$ref":"#/components/schemas/ApierPostOrchestratorErrorCode","description":"Stable Apier error code from the reliability layer's normalizer, narrowed to the post-orchestrator subset (round 6). Generated SDK clients see a 10-value closed enum for exhaustive switch statements."},"execution_guarantee":{"$ref":"#/components/schemas/ExecutionGuarantee"},"outcome":{"$ref":"#/components/schemas/Outcome"}}}]}},"responses":{"SandboxContextSuccess":{"description":"Sandbox 2xx success for `GET /api/v1/sandbox/company/{org}/context`. Body shape is the typed `SandboxContextEnvelope` — `{ success: true, data: SandboxContextData, _meta: SandboxMeta }`. Per-operation envelope keeps the mirror typing of the production /context endpoint for generated clients.","headers":{"Cache-Control":{"description":"`no-store` — sandbox responses are not cacheable. Public/shared caching would replay the cached `X-Correlation-ID` to every later cache-hit, breaking the per-request trace contract; private/browser caching has the same problem at single-client granularity. The static-fixture model means cache hits would not save meaningful cost. Both 2xx and 4xx/5xx use the same value. See DECISIONS.md PR-074 Vercel Edge Cache + WAF for the full rationale.","schema":{"type":"string","enum":["no-store"]}},"Access-Control-Allow-Origin":{"description":"`*`.","schema":{"type":"string"}},"Access-Control-Allow-Methods":{"description":"`GET, OPTIONS`.","schema":{"type":"string"}},"Access-Control-Allow-Headers":{"description":"`Content-Type, Accept, Authorization, X-Correlation-ID`.","schema":{"type":"string"}},"Access-Control-Expose-Headers":{"description":"`X-Correlation-ID, Link, X-Sandbox`.","schema":{"type":"string"}},"Cross-Origin-Resource-Policy":{"description":"`cross-origin`.","schema":{"type":"string"}},"X-Sandbox":{"description":"Always `true`.","schema":{"type":"string","enum":["true"]}},"X-Content-Type-Options":{"description":"`nosniff`.","schema":{"type":"string","enum":["nosniff"]}},"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"description":"`</openapi.json>; rel=\"service-desc\"` per Rule 4.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SandboxContextEnvelope"}}}},"SandboxObligationsSuccess":{"description":"Sandbox 2xx success for `GET /api/v1/sandbox/company/{org}/obligations`. Body shape is the typed `SandboxObligationsEnvelope` — `{ success: true, data: SandboxObligationsData, _meta: SandboxMeta }`.","headers":{"Cache-Control":{"description":"`no-store` — sandbox responses are not cacheable. Public/shared caching would replay the cached `X-Correlation-ID` to every later cache-hit, breaking the per-request trace contract; private/browser caching has the same problem at single-client granularity. The static-fixture model means cache hits would not save meaningful cost. Both 2xx and 4xx/5xx use the same value. See DECISIONS.md PR-074 Vercel Edge Cache + WAF for the full rationale.","schema":{"type":"string","enum":["no-store"]}},"Access-Control-Allow-Origin":{"description":"`*`.","schema":{"type":"string"}},"Access-Control-Allow-Methods":{"description":"`GET, OPTIONS`.","schema":{"type":"string"}},"Access-Control-Allow-Headers":{"description":"`Content-Type, Accept, Authorization, X-Correlation-ID`.","schema":{"type":"string"}},"Access-Control-Expose-Headers":{"description":"`X-Correlation-ID, Link, X-Sandbox`.","schema":{"type":"string"}},"Cross-Origin-Resource-Policy":{"description":"`cross-origin`.","schema":{"type":"string"}},"X-Sandbox":{"description":"Always `true`.","schema":{"type":"string","enum":["true"]}},"X-Content-Type-Options":{"description":"`nosniff`.","schema":{"type":"string","enum":["nosniff"]}},"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"description":"`</openapi.json>; rel=\"service-desc\"`.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SandboxObligationsEnvelope"}}}},"SandboxDeadlinesSuccess":{"description":"Sandbox 2xx success for `GET /api/v1/sandbox/company/{org}/deadlines`. Body shape is the typed `SandboxDeadlinesEnvelope` — `{ success: true, data: SandboxDeadlinesData, _meta: SandboxMeta }`.","headers":{"Cache-Control":{"description":"`no-store` — sandbox responses are not cacheable. Public/shared caching would replay the cached `X-Correlation-ID` to every later cache-hit, breaking the per-request trace contract; private/browser caching has the same problem at single-client granularity. The static-fixture model means cache hits would not save meaningful cost. Both 2xx and 4xx/5xx use the same value. See DECISIONS.md PR-074 Vercel Edge Cache + WAF for the full rationale.","schema":{"type":"string","enum":["no-store"]}},"Access-Control-Allow-Origin":{"description":"`*`.","schema":{"type":"string"}},"Access-Control-Allow-Methods":{"description":"`GET, OPTIONS`.","schema":{"type":"string"}},"Access-Control-Allow-Headers":{"description":"`Content-Type, Accept, Authorization, X-Correlation-ID`.","schema":{"type":"string"}},"Access-Control-Expose-Headers":{"description":"`X-Correlation-ID, Link, X-Sandbox`.","schema":{"type":"string"}},"Cross-Origin-Resource-Policy":{"description":"`cross-origin`.","schema":{"type":"string"}},"X-Sandbox":{"description":"Always `true`.","schema":{"type":"string","enum":["true"]}},"X-Content-Type-Options":{"description":"`nosniff`.","schema":{"type":"string","enum":["nosniff"]}},"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"description":"`</openapi.json>; rel=\"service-desc\"`.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SandboxDeadlinesEnvelope"}}}},"SandboxSummarySuccess":{"description":"Sandbox 2xx success for `GET /api/v1/sandbox/company/{org}/summary`. Body shape is the typed `SandboxSummaryEnvelope` — `{ success: true, data: SandboxSummaryData, _meta: SandboxMeta }`.","headers":{"Cache-Control":{"description":"`no-store` — sandbox responses are not cacheable. Public/shared caching would replay the cached `X-Correlation-ID` to every later cache-hit, breaking the per-request trace contract; private/browser caching has the same problem at single-client granularity. The static-fixture model means cache hits would not save meaningful cost. Both 2xx and 4xx/5xx use the same value. See DECISIONS.md PR-074 Vercel Edge Cache + WAF for the full rationale.","schema":{"type":"string","enum":["no-store"]}},"Access-Control-Allow-Origin":{"description":"`*`.","schema":{"type":"string"}},"Access-Control-Allow-Methods":{"description":"`GET, OPTIONS`.","schema":{"type":"string"}},"Access-Control-Allow-Headers":{"description":"`Content-Type, Accept, Authorization, X-Correlation-ID`.","schema":{"type":"string"}},"Access-Control-Expose-Headers":{"description":"`X-Correlation-ID, Link, X-Sandbox`.","schema":{"type":"string"}},"Cross-Origin-Resource-Policy":{"description":"`cross-origin`.","schema":{"type":"string"}},"X-Sandbox":{"description":"Always `true`.","schema":{"type":"string","enum":["true"]}},"X-Content-Type-Options":{"description":"`nosniff`.","schema":{"type":"string","enum":["nosniff"]}},"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"description":"`</openapi.json>; rel=\"service-desc\"`.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SandboxSummaryEnvelope"}}}},"SandboxAuditSuccess":{"description":"Sandbox 2xx success for `GET /api/v1/sandbox/company/{org}/audit`. Body shape is the typed `SandboxAuditEnvelope` — `{ success: true, data: SandboxAuditData, _meta: SandboxMeta }`. Note the double-wrapped data nesting (`data.data` for the rows + `data.pagination` for the cursor block) which mirrors production audit's envelope.","headers":{"Cache-Control":{"description":"`no-store` — sandbox responses are not cacheable. Public/shared caching would replay the cached `X-Correlation-ID` to every later cache-hit, breaking the per-request trace contract; private/browser caching has the same problem at single-client granularity. The static-fixture model means cache hits would not save meaningful cost. Both 2xx and 4xx/5xx use the same value. See DECISIONS.md PR-074 Vercel Edge Cache + WAF for the full rationale.","schema":{"type":"string","enum":["no-store"]}},"Access-Control-Allow-Origin":{"description":"`*`.","schema":{"type":"string"}},"Access-Control-Allow-Methods":{"description":"`GET, OPTIONS`.","schema":{"type":"string"}},"Access-Control-Allow-Headers":{"description":"`Content-Type, Accept, Authorization, X-Correlation-ID`.","schema":{"type":"string"}},"Access-Control-Expose-Headers":{"description":"`X-Correlation-ID, Link, X-Sandbox`.","schema":{"type":"string"}},"Cross-Origin-Resource-Policy":{"description":"`cross-origin`.","schema":{"type":"string"}},"X-Sandbox":{"description":"Always `true`.","schema":{"type":"string","enum":["true"]}},"X-Content-Type-Options":{"description":"`nosniff`.","schema":{"type":"string","enum":["nosniff"]}},"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"description":"`</openapi.json>; rel=\"service-desc\"`.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SandboxAuditEnvelope"}}}},"SandboxError":{"description":"Sandbox 4xx / 5xx error. Body shape mirrors the production error envelope (`{ success: false, error_code, explanation, _meta }`); `explanation` is the Compliance Explainer's full Norwegian-bokmål `{ summary, why, fix_steps[], relevant_link, legal_basis, handover }`. The Cache-Control header is `no-store` so transient failures aren't pinned in the Edge cache.","headers":{"Cache-Control":{"description":"`no-store` on errors — transient failures must not be cached.","schema":{"type":"string","enum":["no-store"]}},"Access-Control-Allow-Origin":{"description":"`*` — same CORS posture as success responses.","schema":{"type":"string"}},"Access-Control-Expose-Headers":{"description":"`X-Correlation-ID, Link, X-Sandbox` — same set as success responses so browser agents can read these cross-origin even on the error path.","schema":{"type":"string"}},"X-Sandbox":{"description":"Always `true` — present on errors too so agents detecting sandbox via header don't depend on body parsing.","schema":{"type":"string","enum":["true"]}},"X-Content-Type-Options":{"description":"`nosniff`.","schema":{"type":"string","enum":["nosniff"]}},"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"description":"`</openapi.json>; rel=\"service-desc\"` per Rule 4.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SandboxEnvelopeError"}}}},"SandboxOptionsPreflight":{"description":"CORS preflight response. 204 No Content with the standard sandbox CORS headers. Browsers cache the preflight via `Access-Control-Max-Age` (1 hour); the response itself is not cached by intermediaries.","headers":{"Cache-Control":{"description":"`no-store` — keeps the HTTP response itself out of intermediary caches. Mirrors the value applied to every other sandbox 2xx/4xx/5xx response (runtime sets it via `applySandboxHeaders` in src/lib/sandbox/headers.ts, which the OPTIONS helper also calls). Browser preflight caching still uses `Access-Control-Max-Age` independently.","schema":{"type":"string","enum":["no-store"]}},"Access-Control-Allow-Origin":{"description":"`*`.","schema":{"type":"string"}},"Access-Control-Allow-Methods":{"description":"`GET, OPTIONS`.","schema":{"type":"string"}},"Access-Control-Allow-Headers":{"description":"`Content-Type, Accept, Authorization, X-Correlation-ID` — same set as the sandbox 2xx responses.","schema":{"type":"string"}},"Access-Control-Expose-Headers":{"description":"`X-Correlation-ID, Link, X-Sandbox` — same set as the success response so browser agents can read these cross-origin.","schema":{"type":"string"}},"Access-Control-Max-Age":{"description":"`3600` — browsers cache the preflight for an hour.","schema":{"type":"string","enum":["3600"]}},"X-Content-Type-Options":{"description":"`nosniff` — disables MIME-sniffing on the empty 204 body. Mirrors the runtime emission stamped by `applyCommon` in src/lib/sandbox/headers.ts on every sandbox response, including preflights.","schema":{"type":"string","enum":["nosniff"]}},"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"X-Sandbox":{"description":"Always `true`.","schema":{"type":"string","enum":["true"]}},"Link":{"description":"`</openapi.json>; rel=\"service-desc\"` per CLAUDE.md Rule 4 — set by the proxy on every /api/v1/* response, INCLUDING preflights, so the Expose-Headers list above is honoured by browsers reading it on a 204.","schema":{"type":"string"}}}},"PublicSandboxOptionsPreflight":{"description":"CORS preflight for the unauthenticated public-sandbox surface (`/api/v1/sandbox/public/*`). Differs from the internal `SandboxOptionsPreflight` in three places: (1) Allow-Methods is `GET, POST, OPTIONS` — the public surface is unified across both verbs because every public-sandbox endpoint shares one handler factory; (2) `X-Robots-Tag: noindex` is set so the JSON API surface never lands in a search index even if a crawler stumbles onto it; (3) `X-Apier-Sandbox: public` is set as the agent-facing detection signal (SDKs branch on this header to distinguish the public from the internal sandbox without parsing the URL). `Access-Control-Allow-Credentials` is INTENTIONALLY absent — public sandbox is zero-auth and must not advertise credential support (PR-SANDBOX-PUBLIC).","headers":{"Cache-Control":{"description":"`no-store`.","schema":{"type":"string","enum":["no-store"]}},"Access-Control-Allow-Origin":{"description":"`*`.","schema":{"type":"string"}},"Access-Control-Allow-Methods":{"description":"`GET, POST, OPTIONS` — unified across the public-sandbox surface.","schema":{"type":"string","enum":["GET, POST, OPTIONS"]}},"Access-Control-Allow-Headers":{"description":"`Content-Type, Accept, Authorization, X-Correlation-ID, Idempotency-Key` — same set as the internal sandbox preflight (so browser callers reusing a shared HTTP client stack don't choke on a preflight rejection) PLUS `Idempotency-Key`, which `/api/v1/sandbox/public/actions/execute` REQUIRES and the other public POST routes accept as optional (CLAUDE.md Rule 5). Authorization is listed but the public surface IGNORES it (zero-auth).","schema":{"type":"string"}},"Access-Control-Max-Age":{"description":"`3600` — browsers cache the preflight for an hour.","schema":{"type":"string","enum":["3600"]}},"X-Robots-Tag":{"description":"`noindex` — keeps the JSON API surface out of search-engine indexes.","schema":{"type":"string","enum":["noindex"]}},"X-Apier-Sandbox":{"description":"`public` — agent-facing detection signal so SDKs can distinguish the public from the internal sandbox without parsing the URL.","schema":{"type":"string","enum":["public"]}},"X-Sandbox":{"description":"Always `true`.","schema":{"type":"string","enum":["true"]}},"X-Content-Type-Options":{"description":"`nosniff`.","schema":{"type":"string","enum":["nosniff"]}},"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"description":"`</openapi.json>; rel=\"service-desc\"` per Rule 4.","schema":{"type":"string"}}}},"PublicSandboxError":{"description":"4xx / 5xx error from a public-sandbox route (`/api/v1/sandbox/public/*`). Body shape uses the public-surface error_code vocabulary defined in `PublicSandboxEnvelopeError` (adds `PUBLIC_SANDBOX_ORG_NOT_PERMITTED`, `PUBLIC_SANDBOX_BODY_TOO_LARGE`, `RATE_LIMIT_EXCEEDED` on top of the internal sandbox set). Response carries the public-sandbox-specific header set: `Access-Control-Allow-Methods: GET, POST, OPTIONS`, `X-Robots-Tag: noindex`, `X-Apier-Sandbox: public`. `Access-Control-Allow-Credentials` is INTENTIONALLY absent. On 429 responses, `Retry-After` carries the seconds-until-bucket-reset for the per-IP rate-limit window.","headers":{"Cache-Control":{"description":"`no-store` on errors — transient failures must not be cached.","schema":{"type":"string","enum":["no-store"]}},"Access-Control-Allow-Origin":{"description":"`*`.","schema":{"type":"string"}},"Access-Control-Allow-Methods":{"description":"`GET, POST, OPTIONS` — unified across the public-sandbox surface (the internal `SandboxError` advertises `GET, OPTIONS` which would be wrong on these routes).","schema":{"type":"string","enum":["GET, POST, OPTIONS"]}},"Access-Control-Expose-Headers":{"description":"`X-Correlation-ID, Link, X-Sandbox, X-Apier-Upstream-Healthy` — same expose-set as the internal sandbox.","schema":{"type":"string"}},"X-Robots-Tag":{"description":"`noindex`.","schema":{"type":"string","enum":["noindex"]}},"X-Apier-Sandbox":{"description":"`public`.","schema":{"type":"string","enum":["public"]}},"X-Sandbox":{"description":"Always `true` — present on errors too so agents detecting sandbox via header don't depend on body parsing.","schema":{"type":"string","enum":["true"]}},"X-Content-Type-Options":{"description":"`nosniff`.","schema":{"type":"string","enum":["nosniff"]}},"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Retry-After":{"description":"Set on 429 responses only — seconds-until-bucket-reset for the per-IP rate-limit window (100 requests / hour, 'public-sandbox' bucket).","schema":{"type":"string"}},"Link":{"description":"`</openapi.json>; rel=\"service-desc\"` per Rule 4.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicSandboxEnvelopeError"}}}},"SandboxWriteSideOptionsPreflight":{"description":"CORS preflight response for the PR-074-sandbox-execute write-side endpoints (`/api/v1/sandbox/auth/approval-token`, `/api/v1/sandbox/actions/plan`, `/api/v1/sandbox/actions/execute`). Differs from `SandboxOptionsPreflight` in four deliberate ways: (1) Allow-Methods is `POST, OPTIONS` not `GET, OPTIONS`; (2) Allow-Headers list includes `Authorization` (PR-SCOPE-002 — sandbox is auth-gated; browser preflights must allow the header or the cross-origin POST fails CORS before the handler runs); (3) `Access-Control-Allow-Credentials: false` is set explicitly (Bearer tokens flow cross-origin with credentials=false; only cookies require credentials=true); (4) Max-Age is 86400 (24h, not 1h) — preflight stable across the entire deploy.","headers":{"Cache-Control":{"description":"`no-store` — same posture as the read-side preflight.","schema":{"type":"string","enum":["no-store"]}},"Access-Control-Allow-Origin":{"description":"`*`.","schema":{"type":"string"}},"Access-Control-Allow-Methods":{"description":"`POST, OPTIONS` — the write-side endpoints accept POST only.","schema":{"type":"string","enum":["POST, OPTIONS"]}},"Access-Control-Allow-Headers":{"description":"Per-route safe list — `Authorization, Content-Type, Idempotency-Key, X-Correlation-ID` on all three write-side endpoints. PR-SCOPE-002: `Authorization` is now in this list because the sandbox is auth-gated; `sandboxWriteSideOptionsResponse` throws at runtime if a caller drops it. `X-Correlation-ID` is advertised because the POST operations declare it as an input parameter — browser clients must be allowed to send it without a preflight rejection.","schema":{"type":"string"}},"Access-Control-Allow-Credentials":{"description":"`false` — mandatory on every write-side sandbox response. Bearer tokens flow cross-origin with credentials=false; only cookies require credentials=true. Sandbox uses Bearer exclusively (PR-SCOPE-002).","schema":{"type":"string","enum":["false"]}},"Access-Control-Expose-Headers":{"description":"`X-Correlation-ID, Link, X-Sandbox, X-Apier-Upstream-Healthy` — `X-Apier-Upstream-Healthy` added in iteration 8 so the PR-A reliability header `/api/v1/sandbox/actions/execute` emits is reachable by browser agents (read-side endpoints never emit it; listing is a no-op there but keeps the surface consistent).","schema":{"type":"string"}},"Access-Control-Max-Age":{"description":"`86400` (24 hours) — preflight is stable across the entire deploy. Repeat callers don't pay preflight cost on every POST.","schema":{"type":"string","enum":["86400"]}},"X-Content-Type-Options":{"description":"`nosniff`.","schema":{"type":"string","enum":["nosniff"]}},"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"X-Sandbox":{"description":"Always `true`.","schema":{"type":"string","enum":["true"]}},"Link":{"description":"`</openapi.json>; rel=\"service-desc\"` per CLAUDE.md Rule 4.","schema":{"type":"string"}}}},"SandboxWriteSideError":{"description":"Sandbox 4xx / 5xx error from a PR-074-sandbox-execute write-side endpoint (`/api/v1/sandbox/auth/approval-token`, `/api/v1/sandbox/actions/plan`). Body shape mirrors `SandboxError` (the read-side error envelope) but the response carries the write-side CORS contract that `applySandboxWriteSideCorsHeaders` stamps on every error path: `Access-Control-Allow-Methods: POST, OPTIONS`, `Access-Control-Allow-Credentials: false`, and the write-side `Access-Control-Allow-Headers` safe list. Iteration-17 polish (CodeRabbit Major): introduced so the published contract for approval-token + plan errors stops drifting from the runtime — they previously reused `SandboxError` which advertises the read-side `GET, OPTIONS` Allow-Methods and omits the explicit `Allow-Credentials: false` write-side guarantee. The execute route uses `SandboxExecuteError` instead because it ships PR-A reliability fields (`execution_guarantee` + `outcome`) that approval-token + plan do NOT emit.","headers":{"Cache-Control":{"description":"`no-store` on errors — transient failures must not be cached.","schema":{"type":"string","enum":["no-store"]}},"Access-Control-Allow-Origin":{"description":"`*`.","schema":{"type":"string"}},"Access-Control-Allow-Methods":{"description":"`POST, OPTIONS` — the write-side endpoints accept POST only. Read-side `SandboxError` advertises `GET, OPTIONS` which would be wrong on these routes.","schema":{"type":"string","enum":["POST, OPTIONS"]}},"Access-Control-Allow-Headers":{"description":"`Content-Type, Idempotency-Key, X-Correlation-ID` — same write-side safe list as the OPTIONS preflight (`SandboxWriteSideOptionsPreflight`). `Authorization` is NEVER in this list; sandbox is zero-auth.","schema":{"type":"string"}},"Access-Control-Allow-Credentials":{"description":"`false` — mandatory on every write-side sandbox response. Bearer tokens flow cross-origin with credentials=false; only cookies require credentials=true. Sandbox uses Bearer exclusively (PR-SCOPE-002).","schema":{"type":"string","enum":["false"]}},"Access-Control-Expose-Headers":{"description":"`X-Correlation-ID, Link, X-Sandbox, X-Apier-Upstream-Healthy` — same expose-set as every other sandbox response (`applyCommon` in `src/lib/sandbox/headers.ts` stamps it universally). `X-Apier-Upstream-Healthy` is a no-op on approval-token + plan (only execute emits it) but listing it keeps the expose-set byte-stable across the surface.","schema":{"type":"string"}},"X-Sandbox":{"description":"Always `true` — present on errors too so agents detecting sandbox via header don't depend on body parsing.","schema":{"type":"string","enum":["true"]}},"X-Content-Type-Options":{"description":"`nosniff`.","schema":{"type":"string","enum":["nosniff"]}},"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"description":"`</openapi.json>; rel=\"service-desc\"` per Rule 4.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SandboxWriteSideEnvelopeErrorClosed"}}}},"SandboxExecuteError":{"description":"Sandbox 4xx / 5xx error from `/api/v1/sandbox/actions/execute`. Mirrors `SandboxError`'s envelope but adds the PR-A reliability fields `execution_guarantee` + `outcome` at top level (universal-emission contract — every action response, success or error, ships the reliability envelope so SDK parsers don't have to branch on shape). Denied paths emit `outcome.is_final=true`, `outcome.requires_followup=true`, `outcome.success_probability=0`, `outcome.confidence='high'`; `execution_guarantee.circuit_state='OPEN'`. The `X-Apier-Upstream-Healthy` response header is also set on every error path.","headers":{"Cache-Control":{"description":"`no-store`.","schema":{"type":"string","enum":["no-store"]}},"Access-Control-Allow-Origin":{"description":"`*`.","schema":{"type":"string"}},"Access-Control-Allow-Methods":{"description":"`POST, OPTIONS` — write-side override of the read-side `GET, OPTIONS` default. Iteration-17 polish (CodeRabbit Major): added so the documented contract surfaces what `applySandboxWriteSideCorsHeaders` stamps on every error response.","schema":{"type":"string","enum":["POST, OPTIONS"]}},"Access-Control-Allow-Headers":{"description":"`Authorization, Content-Type, Idempotency-Key, X-Correlation-ID` — write-side safe list. PR-SCOPE-002: `Authorization` is on this list because the sandbox is auth-gated. Iteration-17 polish (CodeRabbit Major): same justification as Allow-Methods.","schema":{"type":"string"}},"Access-Control-Allow-Credentials":{"description":"`false` — mandatory on every write-side sandbox response.","schema":{"type":"string","enum":["false"]}},"X-Sandbox":{"description":"Always `true`.","schema":{"type":"string","enum":["true"]}},"X-Apier-Upstream-Healthy":{"description":"`true` or `false` — PR-A reliability signal. Set on every action response (success or error).","schema":{"type":"string","enum":["true","false"]}},"X-Content-Type-Options":{"description":"`nosniff`.","schema":{"type":"string","enum":["nosniff"]}},"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"description":"`</openapi.json>; rel=\"service-desc\"`.","schema":{"type":"string"}},"Access-Control-Expose-Headers":{"description":"`X-Correlation-ID, Link, X-Sandbox, X-Apier-Upstream-Healthy` — same expose-set as the success response so browser clients can read `X-Apier-Upstream-Healthy` cross-origin on the error path too. Iteration-10 polish (CodeRabbit Major): the dedicated SandboxExecuteError component had previously dropped this header from the documented set, leaving the docs out of sync with the runtime (`applyCommon` in `src/lib/sandbox/headers.ts` always emits it).","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SandboxExecuteEnvelopeError"}}}},"Unauthorized":{"description":"Authentication failed — missing, malformed, invalid, or revoked API key.","headers":{"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"$ref":"#/components/headers/LinkServiceDescriptorHeader"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorWithStrictProvenance"},"examples":{"missing_header":{"summary":"No Authorization header provided","value":{"success":false,"error_code":"AUTH_MISSING","explanation":{"summary":"Missing Authorization header"},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-16T12:00:00.000Z","last_verified":"2026-04-16T12:00:00.000Z","source":"apier.no","schema_version":"1.0.5","response_timestamp":"2026-04-25T12:00:00.000Z","response_hash":"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}}},"malformed_header":{"summary":"Authorization header not using Bearer scheme","value":{"success":false,"error_code":"AUTH_MALFORMED","explanation":{"summary":"Malformed Authorization header"},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-16T12:00:00.000Z","last_verified":"2026-04-16T12:00:00.000Z","source":"apier.no","schema_version":"1.0.5","response_timestamp":"2026-04-25T12:00:00.000Z","response_hash":"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}}},"invalid_key":{"summary":"API key not recognized","value":{"success":false,"error_code":"AUTH_INVALID_KEY","explanation":{"summary":"Invalid API key"},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-16T12:00:00.000Z","last_verified":"2026-04-16T12:00:00.000Z","source":"apier.no","schema_version":"1.0.5","response_timestamp":"2026-04-25T12:00:00.000Z","response_hash":"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}}},"revoked_key":{"summary":"API key has been revoked","value":{"success":false,"error_code":"AUTH_KEY_REVOKED","explanation":{"summary":"API key has been revoked"},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-16T12:00:00.000Z","last_verified":"2026-04-16T12:00:00.000Z","source":"apier.no","schema_version":"1.0.5","response_timestamp":"2026-04-25T12:00:00.000Z","response_hash":"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}}}}}}},"RateLimitExceeded":{"description":"Rate limit reached on at least one of TWO independent dimensions running in parallel: per-(api_key, endpoint) per-minute (the established Rule 27 per-tier scaling — Free / Starter / Professional / Enterprise unlimited) AND per-(api_key, org_number) per-day (Amendment 61 §5.2 — bounds runaway-agent failure modes per client org so one org's traffic cannot starve siblings sharing the same key; daily bucket window is one calendar day in Europe/Oslo, DST-aware). The `X-RateLimit-Dimension` header identifies which bucket tripped: `per_key`, `per_org`, or `both` (when the same request exceeds both — the binding Retry-After is the per-org value because the per-org window is hours/days vs the per-minute window). The `Retry-After` header indicates how many seconds to wait before the binding window resets; `X-RateLimit-Limit` / `X-RateLimit-Remaining` / `X-RateLimit-Reset` describe the per-minute bucket state. The per-day per-org limits per tier (sized at ~100× typical daily volume — the dimension exists to bound failure modes, not to monetize) are: Free 1000/org/day, Starter 5000, Professional 25000, Enterprise unlimited. On rare infrastructure-side failures the gateway fails OPEN and passes the request through with `X-RateLimit-Check: skipped` on the response — clients can treat that as a transient signal that quota enforcement was briefly degraded.","headers":{"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"$ref":"#/components/headers/LinkServiceDescriptorHeader"},"Retry-After":{"description":"Seconds to wait before retrying. Integer, always >= 1. When `X-RateLimit-Dimension` is `per_org` or `both`, this is seconds-until-next-Oslo-midnight (DST-aware via Postgres timezone conversion); for `per_key` it is seconds-until-next-minute-window.","schema":{"type":"integer","minimum":1}},"X-RateLimit-Dimension":{"description":"Which rate-limit dimension tripped on this request: `per_key` (per-(api_key, endpoint) per-minute bucket, the Rule 27 cap), `per_org` (per-(api_key, org_number) per-day bucket, Amendment 61 §5.2), or `both` (the same request exceeded BOTH simultaneously). When the header is `both`, exactly ONE 429 + audit row is emitted and the `Retry-After` header carries the per-org value (the longer of the two windows — per-day vs per-minute). Present on every 429 from authenticated endpoints. Absent on the public per-IP path (which is conceptually a per-IP dimension, not authenticated).","schema":{"type":"string","enum":["per_key","per_org","both"]}},"X-RateLimit-Limit":{"description":"The per-minute limit that applied to this bucket. Present whenever the per-key RPC observed an authoritative count — even when `X-RateLimit-Dimension: per_org` was the binding cap (the per-key bucket state is still useful diagnostic). Absent when the per-key RPC infrastructure-failed (no authoritative count to emit).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"The PER-KEY bucket's remaining capacity at the time of the 429 — `Math.max(0, X-RateLimit-Limit - count)`. On a `per_key`-binding 429 this is 0 by definition. On a `per_org`-binding 429 it can be POSITIVE (the per-key bucket may still have headroom even though the per-day per-org bucket binds), so SDK consumers MUST treat this header as informational, not a guarantee that retry will succeed. Subject to the same presence rules as `X-RateLimit-Limit`.","schema":{"type":"integer","minimum":0}},"X-RateLimit-Reset":{"description":"Unix-seconds timestamp when the per-minute window closes. Subject to the same presence rules as `X-RateLimit-Limit`.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorWithStrictProvenance"},"examples":{"rate_limit_exceeded_per_key":{"summary":"Per-(api_key, endpoint) per-minute ceiling reached (X-RateLimit-Dimension: per_key)","value":{"success":false,"error_code":"RATE_LIMIT_EXCEEDED","explanation":{"summary":"Rate limit exceeded","why":"Grense per minutt for dette endepunktet er nådd. Se X-RateLimit-Limit-header for den nøyaktige grensen som gjelder dette nøkkelen og kategorien (A eller B). Vent til X-RateLimit-Reset før du prøver igjen.","fix_steps":["Vent på sekundene angitt i Retry-After eller X-RateLimit-Reset","Oppgrader abonnementet for høyere grenser"]},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-18T13:00:00.000Z","last_verified":"2026-04-18T13:00:00.000Z","source":"apier.no","schema_version":"1.0.5","response_timestamp":"2026-04-25T12:00:00.000Z","response_hash":"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}}},"rate_limit_exceeded_per_org":{"summary":"Per-(api_key, org_number) per-day ceiling reached — Amendment 61 §5.2 (X-RateLimit-Dimension: per_org)","value":{"success":false,"error_code":"RATE_LIMIT_EXCEEDED","explanation":{"summary":"Rate limit exceeded","why":"Daglig grense per (API-nøkkel × organisasjon 123456789) er nådd (1000 forespørsler per dag). Vent til neste midnatt i Europe/Oslo før neste forsøk.","fix_steps":["Vent til neste Oslo-midnatt (se Retry-After) og prøv igjen","Oppgrader abonnementet for høyere grenser"]},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-18T13:00:00.000Z","last_verified":"2026-04-18T13:00:00.000Z","source":"apier.no","schema_version":"1.0.5","response_timestamp":"2026-05-04T12:00:00.000Z","response_hash":"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}}}}}}},"RulebookRateLimitExceeded":{"description":"Authenticated rate limit reached on a Rulebook-influenced endpoint (`/v1/company/{org}/obligations`, `/v1/company/{org}/summary`). Same two-dimensional shape as `RateLimitExceeded` (per-(api_key, endpoint) per-minute AND per-(api_key, org_number) per-day, with `X-RateLimit-Dimension: per_key | per_org | both` identifying which bucket tripped). Body schema is `RulebookApiErrorWithStrictProvenance` — composes the Rulebook trust-metadata contract (`_meta.data_source` + `_meta.legal_basis` REQUIRED, CLAUDE.md Rule 3) AND the strict-provenance contract (`_meta.response_timestamp` + `_meta.response_hash` REQUIRED, Amendment 001 Feature A) so clients see NEITHER side weakened on the 429 path. The generic `RateLimitExceeded` uses `ApiErrorWithStrictProvenance` which drops Rulebook fields; a Rulebook-only envelope would drop response_timestamp + response_hash. This composite component is the canonical fix and the only 429 component on the Rulebook surface.","headers":{"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"$ref":"#/components/headers/LinkServiceDescriptorHeader"},"Retry-After":{"description":"Seconds to wait before retrying. Integer, always >= 1. When `X-RateLimit-Dimension` is `per_org` or `both`, this is seconds-until-next-Oslo-midnight (DST-aware via Postgres timezone conversion); for `per_key` it is seconds-until-next-minute-window.","schema":{"type":"integer","minimum":1}},"X-RateLimit-Dimension":{"description":"Which rate-limit dimension tripped on this request: `per_key` (per-(api_key, endpoint) per-minute bucket, the Rule 27 cap), `per_org` (per-(api_key, org_number) per-day bucket, Amendment 61 §5.2), or `both`. Same enum semantics as the generic `RateLimitExceeded` X-RateLimit-Dimension header.","schema":{"type":"string","enum":["per_key","per_org","both"]}},"X-RateLimit-Limit":{"description":"The per-minute limit that applied to this bucket. Same presence rules as on the generic component.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"PER-KEY bucket's remaining capacity at the time of the 429. Can be POSITIVE on a `per_org`-binding 429.","schema":{"type":"integer","minimum":0}},"X-RateLimit-Reset":{"description":"Unix-seconds timestamp when the per-minute window closes.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RulebookApiErrorWithStrictProvenance"}}}},"PublicRateLimitExceeded":{"description":"Per-minute rate limit reached for this (IP, endpoint) bucket on a zero-auth public endpoint. The `Retry-After` header indicates how many seconds to wait before the window resets. Public endpoints share a single soft-cap of 1000 requests per minute PER IP — there is no tier scaling, because callers aren't authenticated. Every allowed response also carries the standard `X-RateLimit-*` headers. On rare infrastructure-side failures the gateway fails OPEN and passes the request through with `X-RateLimit-Check: skipped`.","headers":{"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"$ref":"#/components/headers/LinkServiceDescriptorHeader"},"Retry-After":{"description":"Seconds to wait before retrying. Integer, always >= 1.","schema":{"type":"integer","minimum":1}},"X-RateLimit-Limit":{"description":"Always 1000 — the public per-IP ceiling.","schema":{"type":"integer","enum":[1000]}},"X-RateLimit-Remaining":{"description":"Always 0 on a 429 response.","schema":{"type":"integer","enum":[0]}},"X-RateLimit-Reset":{"description":"Unix-seconds timestamp when the current window closes.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorWithStrictProvenance"},"examples":{"public_rate_limit_exceeded":{"summary":"Public endpoint per-IP 1000/min cap hit","value":{"success":false,"error_code":"RATE_LIMIT_EXCEEDED","explanation":{"summary":"Rate limit exceeded","why":"Grense per minutt for dette endepunktet er nådd (1000 forespørsler). Vent 20 sekunder før du prøver igjen.","fix_steps":["Vent 20 sekunder og prøv igjen","Reduser forespørselsraten eller fordel trafikken over tid"]},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-19T15:00:00.000Z","last_verified":"2026-04-19T15:00:00.000Z","source":"apier.no","schema_version":"1.0.5","response_timestamp":"2026-04-25T12:00:00.000Z","response_hash":"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}}}}}}},"Conflict409":{"description":"PR-AGENT-CONFLICT-LOCK (Amendment 61 §5.1) — concurrent write detected on the same `(org_number, action_type, resource_id, namespace)` tuple within the per-action lock TTL. The body carries BOTH the spec literal `error: 'concurrent_write_in_flight'` (top-level Amendment 61 token) AND the Compliance Explainer schema (`error_code: 'CONCURRENT_WRITE_IN_FLIGHT'` + Norwegian `explanation`). The blocking request's correlation_id is returned as `first_correlation_id` — strictly a correlation/escalation/forensic signal that does NOT enable receipt replay (only the original consumer holds the Idempotency-Key). The blocked second agent's two actionable paths are: (a) wait `retry_after_seconds` and retry with a fresh Idempotency-Key, or (b) if the SAME consumer already controls the original Idempotency-Key, retry with that key to receive the cached response once the holder completes. Cross-vendor `first_correlation_id` exposure is intentional multi-agent coordination per Amendment 61 §5.1 — correlation_id is a high-entropy random UUID with no covert state. The `X-Apier-Skip-Idempotency-Cache: 1` response header signals that this 409 is NOT cached by the Idempotency-Key middleware (the existing middleware already only caches 2xx; the header is the published forward-compat contract).","headers":{"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"$ref":"#/components/headers/LinkServiceDescriptorHeader"},"Retry-After":{"description":"Seconds the client should wait before the conflict lock TTL expires. Always >= 1 (server clamps via GREATEST(1, CEIL(...)) to prevent thundering-herd retries on sub-second windows).","schema":{"type":"integer","minimum":1}},"X-Apier-Skip-Idempotency-Cache":{"description":"Always `1` on conflict-lock 409 responses. Signals to the Idempotency-Key middleware that this transient 4xx must not be cached — caching a 409 would freeze the conflict for the full Idempotency-Key TTL (24h), permanently breaking retry semantics after the lock TTL expires. The current middleware (PR-024) already only caches 2xx; this header is the published forward-compat contract.","schema":{"type":"string","enum":["1"]}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Conflict409Body"},"examples":{"concurrent_write_in_flight":{"summary":"Concurrent write detected — Amendment 61 §5.1","value":{"success":false,"error":"concurrent_write_in_flight","error_code":"CONCURRENT_WRITE_IN_FLIGHT","first_correlation_id":"11111111-1111-4111-8111-111111111111","retry_after_seconds":540,"explanation":{"summary":"En annen forespørsel for samme handling er allerede under behandling.","why":"Apier oppdaget en samtidig skriving for samme organisasjon, handlingstype og ressurs (handling: mva_melding). For å unngå dobbeltinnsending blokkeres den andre forespørselen til den første er ferdig eller utløper.","fix_steps":["Hvis du selv eier den opprinnelige Idempotency-Key (samme konsument): bruk den nøkkelen for å hente det lagrede svaret når blokkeringen utløper.","Hvis du er en annen agent: vent 540 sekunder og prøv på nytt med en frisk Idempotency-Key, eller eskaler til menneskelig operatør."],"relevant_link":"https://apier.no/trust#write-conflict-detection"},"execution_guarantee":{"retried":0,"retries_remaining":0,"normalized":false,"upstream_healthy":true,"circuit_state":"CLOSED","upstream_system":"altinn","execution_time_ms":0},"outcome":{"success_probability":0.2,"confidence":"low","is_final":false,"requires_followup":true,"followup_action":"retry"},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-05-04T16:00:00.000Z","last_verified":"2026-05-04T16:00:00.000Z","source":"apier.no","schema_version":"1.0.5","response_timestamp":"2026-05-04T16:00:00.000Z","response_hash":"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}}}}}}},"SafetyUnavailable503":{"description":"PR-AGENT-CONFLICT-LOCK (Amendment 61 §5.1) — fail-CLOSED. The conflict-lock acquire RPC was unavailable (DB infra hiccup, RPC drift). Distinct from the per-key/per-org rate-limit fail-OPEN posture: serving traffic without verifying that no other agent is mid-write would risk dual submission, which is the exact failure mode the gate exists to prevent. Body uses the Compliance Explainer schema in Norwegian (Rule 24); Amendment 61 §5.1 does not specify a 503 spec literal so the `error` field mirrors `error_code` for client parity. The `X-Apier-Skip-Idempotency-Cache: 1` response header signals that this transient 5xx must not be cached.","headers":{"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"$ref":"#/components/headers/LinkServiceDescriptorHeader"},"X-Apier-Skip-Idempotency-Cache":{"description":"Always `1` on conflict-lock 503 responses. The current Idempotency-Key middleware (PR-024) already skips ≥500; this header is the published forward-compat contract so a future middleware extension that opts a route into 5xx caching cannot accidentally freeze the safety-system-unavailable path.","schema":{"type":"string","enum":["1"]}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SafetyUnavailable503Body"},"examples":{"safety_system_unavailable":{"summary":"Conflict-lock infrastructure unavailable — Amendment 61 §5.1","value":{"success":false,"error":"safety_system_unavailable","error_code":"SAFETY_SYSTEM_UNAVAILABLE","explanation":{"summary":"Sikkerhetssystemet for samtidighetskontroll er midlertidig utilgjengelig.","why":"Apier kunne ikke verifisere at ingen annen samtidig skriving er i gang for denne handlingen. Av sikkerhetshensyn avvises forespørselen heller enn å risikere dobbeltinnsending.","fix_steps":["Prøv igjen om noen sekunder.","Hvis problemet vedvarer, kontroller status på https://apier.no/status.","Bruk samme Idempotency-Key på alle nye forsøk for å unngå utilsiktet dobbeltinnsending når tjenesten er tilbake."],"relevant_link":"https://apier.no/trust#write-conflict-detection"},"execution_guarantee":{"retried":0,"retries_remaining":0,"normalized":false,"upstream_healthy":true,"circuit_state":"CLOSED","upstream_system":"altinn","execution_time_ms":0},"outcome":{"success_probability":0.2,"confidence":"low","is_final":false,"requires_followup":true,"followup_action":"retry"},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-05-04T16:00:00.000Z","last_verified":"2026-05-04T16:00:00.000Z","source":"apier.no","schema_version":"1.0.5","response_timestamp":"2026-05-04T16:00:00.000Z","response_hash":"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}}}}}}},"IdempotencyInProgress":{"description":"Another request with this Idempotency-Key is still being processed. Clients should wait and retry (Retry-After header indicates seconds).","headers":{"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"$ref":"#/components/headers/LinkServiceDescriptorHeader"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimitHeader"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemainingHeader"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitResetHeader"},"Retry-After":{"description":"Seconds the client should wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorWithStrictProvenance"},"examples":{"in_progress":{"summary":"Concurrent request still running","value":{"success":false,"error_code":"IDEMPOTENCY_IN_PROGRESS","explanation":{"summary":"A request with this Idempotency-Key is already in progress","why":"En forespørsel med samme Idempotency-Key er under behandling. Prøv igjen om noen sekunder.","fix_steps":["Vent til forrige forsøk er ferdig, og prøv så igjen","Endre ikke forespørselskroppen mellom forsøkene"]},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-18T10:00:00.000Z","last_verified":"2026-04-18T10:00:00.000Z","source":"apier.no","schema_version":"1.0.5","response_timestamp":"2026-04-25T12:00:00.000Z","response_hash":"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}}}}}}},"IdempotencyMismatch":{"description":"This Idempotency-Key was previously used with a different request body. Idempotency keys must be unique per unique request.","headers":{"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"$ref":"#/components/headers/LinkServiceDescriptorHeader"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimitHeader"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemainingHeader"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitResetHeader"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorWithStrictProvenance"},"examples":{"mismatch":{"summary":"Same key, different payload","value":{"success":false,"error_code":"IDEMPOTENCY_KEY_MISMATCH","explanation":{"summary":"Idempotency-Key already used with a different payload","why":"Samme Idempotency-Key er tidligere brukt med en annen forespørselskropp. Idempotency-nøkler må være unike per unik forespørsel.","fix_steps":["Bruk en ny Idempotency-Key for denne forespørselen","Eller send samme kropp som forrige forsøk dersom målet er å gjenta"]},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-18T10:00:00.000Z","last_verified":"2026-04-18T10:00:00.000Z","source":"apier.no","schema_version":"1.0.5","response_timestamp":"2026-04-25T12:00:00.000Z","response_hash":"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}}}}}}}},"parameters":{"SandboxSimulateError":{"name":"simulate_error","in":"query","required":false,"schema":{"type":"string","enum":["missing_delegation","invalid_token","validation_error","scope_missing","AUTH_MISSING_DELEGATION","AUTH_EXPIRED_TOKEN","VALIDATION_FAILED","SCOPE_MISSING"]},"description":"Force the sandbox to return a structured error response. Per-token status mapping: `missing_delegation` → 403 AUTH_MISSING_DELEGATION; `invalid_token` → 401 AUTH_EXPIRED_TOKEN; `validation_error` → 400 VALIDATION_FAILED; `scope_missing` → 403 SCOPE_MISSING. The 500 INTERNAL_ERROR path documented on `SandboxEnvelopeError.error_code` is NOT reachable via this parameter; it is reserved for unexpected internal handler failures. Accepts BOTH the lowercase public tokens (CLAUDE.md Rule 28 — primary contract, advertised on `/api/v1/capabilities.sandbox.supported_error_codes`) AND the uppercase internal explainer codes (Rule 10 append-only — back-compat for agents that integrated against the uppercase form). Wins precedence over reserved-org defaults — an explicit query value overrides the reserved-org URL. Centralised so the enum + description are a single-place edit across the five sandbox operations."},"IdempotencyKeyHeader":{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":1,"maxLength":255,"pattern":"^[\\x20-\\x7E]+$"},"description":"Optional client-chosen key that makes this write safely retriable. When the same key is presented on a subsequent request with the same body, the gateway returns the original response (with `Idempotent-Replay: true`) instead of re-executing the write. Reservation is scoped to (consumer, endpoint, key) so the same key on different endpoints or different consumers does not collide. TTL: 24 hours. The key must be 1–255 printable ASCII characters — a v4 UUID or ULID is the recommended format. Keys are SHA-256 hashed before storage so they may safely carry client-internal identifiers.","example":"0196-05d1-4ff1-8e2a-0e9b4a5f6c1d"},"CorrelationIdHeader":{"name":"X-Correlation-ID","in":"header","required":false,"schema":{"type":"string","format":"uuid","pattern":"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$"},"description":"Optional request-scope UUID v4 that the client can set to thread its own trace across multiple requests. A valid UUID v4 is reused verbatim on the server; anything malformed or absent is replaced with a fresh `crypto.randomUUID()`. The canonical value is echoed back on the `X-Correlation-ID` response header AND persisted on every matching `audit_log` row — so a forensic query can reconstruct \"every row from request X\" without timestamp-based correlation. Accepted on every `/api/v1/*` operation via the proxy middleware; no per-operation opt-in required. The header is also emitted on every `/api/v1/*` response — see the `X-Correlation-ID` response-header component.","example":"550e8400-e29b-41d4-a716-446655440000"},"AgentVendorHeader":{"name":"X-Agent-Vendor","in":"header","required":false,"schema":{"type":"string","maxLength":64},"description":"PR-AGENT-ATTESTATION (Amendment 61 §5.3) — OPTIONAL forensic-only attestation: the agent vendor / framework that issued this request (e.g. `anthropic`, `openai`, `langchain`). Persisted to `audit_log.agent_vendor` and `mcp_query_log.agent_vendor`; never used for authorization, scope, rate-limit, or idempotency decisioning. The server strips ASCII control chars (0x00–0x1F + 0x7F), Unicode NEL / line / paragraph separators, and BOM before persistence; values exceeding 64 UTF-8 BYTES are byte-boundary clamped (a non-ASCII string declared at 64 chars may exceed the 64-byte ceiling and be clamped further). NEVER echoed in any response body, response header, `_meta` block, error message, or Sentry payload — one-way capture. Omitting this header writes SQL NULL to the persisted columns. The MCP transport equivalent is `clientInfo.attestation.vendor` (Amendment 61 §5.3).","example":"anthropic"},"AgentModelHeader":{"name":"X-Agent-Model","in":"header","required":false,"schema":{"type":"string","maxLength":128},"description":"PR-AGENT-ATTESTATION (Amendment 61 §5.3) — OPTIONAL forensic-only attestation: the agent model id (e.g. `claude-opus-4-7`, `gpt-4o-mini-2024-07-18`). Persisted to `audit_log.agent_model` and `mcp_query_log.agent_model`; identical sanitisation + clamp + non-echo rules as `X-Agent-Vendor` (control-char strip, 128-UTF-8-BYTE ceiling, never visible on any response). Forensic-only: never used for authorization, scope, rate-limit, or idempotency decisioning.","example":"claude-opus-4-7"},"AgentRunIdHeader":{"name":"X-Agent-Run-Id","in":"header","required":false,"schema":{"type":"string","maxLength":256},"description":"PR-AGENT-ATTESTATION (Amendment 61 §5.3) — OPTIONAL forensic-only attestation: an opaque agent-side run / trace identifier (e.g. a LangSmith run id, a vendor-specific session id). Persisted to `audit_log.agent_run_id` and `mcp_query_log.agent_run_id`; identical sanitisation + clamp + non-echo rules as `X-Agent-Vendor` (control-char strip, 256-UTF-8-BYTE ceiling, never visible on any response). Forensic-only: never used for authorization, scope, rate-limit, or idempotency decisioning.","example":"run_abc123-2026-05-04T13:30:00"}},"headers":{"CorrelationIdResponseHeader":{"description":"Canonical request-scope UUID v4 this response was processed under. Always present on every `/api/v1/*` response. Matches the inbound `X-Correlation-ID` request header when the client supplied a valid UUID v4; otherwise a freshly generated UUID the server chose. Clients should log it alongside their own trace ids for cross-system debugging.","schema":{"type":"string","format":"uuid","pattern":"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$"},"example":"550e8400-e29b-41d4-a716-446655440000"},"LinkServiceDescriptorHeader":{"description":"RFC 8288 Web Linking header pointing at the OpenAPI service descriptor. Always present on every `/api/v1/*` response — injected by the Next.js middleware. Lets agent crawlers discover the spec without scraping HTML.","schema":{"type":"string"},"example":"</openapi.json>; rel=\"service-desc\""},"RateLimitLimitHeader":{"description":"Per-minute rate-limit ceiling for this (api_key, endpoint) bucket. Present on every authenticated 2xx/4xx response EXCEPT enterprise-tier requests, which short-circuit unlimited and omit the X-RateLimit-* trio (see RateLimitExceeded.description for the full trio contract). When this header is absent on an authenticated request, treat it as 'enterprise / unlimited' and skip pacing. Tier × category determines the value: Free Category A is 100/min, Starter Category B is 150/min. PR-073: emitted on /api/v1/actions/execute alongside Link + X-Correlation-ID.","schema":{"type":"integer"},"example":300},"RateLimitRemainingHeader":{"description":"Number of requests remaining in the current per-minute window for this (api_key, endpoint) bucket. Present on every authenticated 2xx/4xx response EXCEPT enterprise-tier requests (see X-RateLimit-Limit description). When absent, treat as 'enterprise / unlimited'.","schema":{"type":"integer"},"example":287},"RateLimitResetHeader":{"description":"Unix-epoch seconds when the current rate-limit window resets — clients can compute `reset - now()` to pace themselves without polling 429s. Present on every authenticated 2xx/4xx response EXCEPT enterprise-tier requests (see X-RateLimit-Limit description). When absent, treat as 'enterprise / unlimited'.","schema":{"type":"integer"},"example":1714291260},"ApierUpstreamHealthyHeader":{"description":"PR-A — boolean signal of upstream gov-system health at response time. Mirrors `execution_guarantee.upstream_healthy` in the response body. `\"true\"` when the per-upstream circuit breaker is CLOSED at response time; `\"false\"` when OPEN/HALF_OPEN.\n\nWhen present: the response went through the reliability orchestrator and the breaker snapshot is authoritative. Agents can treat `\"false\"` as a hint to back off subsequent calls to the same upstream until they see `\"true\"` again — the breaker auto-probes via HALF_OPEN after 30s and closes after 3 successful probes.\n\nWhen absent: the response was emitted before the orchestrator ran (validation, scope, idempotency-key checks, body bounds, approval-token rejection) so no authoritative breaker snapshot exists. Agents should NOT infer breaker state from absence — the breaker may be in any state; absence simply means the orchestrator didn't get the chance to read it for this request.\n\nPresent on `/api/v1/actions/execute` post-orchestrator responses: 200 (live-execute branch — the dry-run branch does NOT carry the header because dry-run skips the orchestrator entirely) and 502 (any post-orchestrator failure that preserves the orchestrator's apier_code — regardless of which specific apier_code surfaces in the body's `error_code` field; round 6 CodeRabbit clarification — was previously documented as 502 GOVERNMENT_API_ERROR-only, but round 3 made the route preserve the full apier_code spectrum: GOVERNMENT_API_ERROR, GOVERNMENT_RATE_LIMITED, GOVERNMENT_UNAVAILABLE, GOVERNMENT_VALIDATION_REJECTED, MASKINPORTEN_AUTH_FAILED, ALTINN_DELEGATION_MISSING, EXECUTION_DEADLINE_EXCEEDED, EXECUTION_CIRCUIT_OPEN, RETRY_BUDGET_EXHAUSTED, EXECUTION_FAILED).","schema":{"type":"string","enum":["true","false"]},"example":"true"}}},"security":[{"BearerAuth":[]}],"paths":{"/api/health":{"get":{"summary":"Health check endpoint","description":"Returns the service status and current server timestamp. This endpoint is used by external uptime monitoring (Betterstack). It is intentionally outside the /v1/ namespace as an infrastructure endpoint that must remain stable across API version bumps.","operationId":"healthCheck","tags":["Infrastructure"],"security":[],"responses":{"200":{"description":"Service is healthy.","content":{"application/json":{"schema":{"type":"object","required":["status","timestamp"],"properties":{"status":{"type":"string","enum":["ok"],"description":"Service status."},"timestamp":{"type":"string","format":"date-time","description":"Current server time in ISO 8601 UTC."}}},"examples":{"healthy":{"value":{"status":"ok","timestamp":"2026-04-16T10:30:00.000Z"}}}}}}}}},"/api/v1/actions/execute":{"parameters":[{"$ref":"#/components/parameters/CorrelationIdHeader"}],"post":{"summary":"Validate (dry-run) or submit (live) a filing action","description":"Two discriminated behaviours on the same endpoint, selected by the `?dry_run=true` query parameter.\n\n**Dry-run mode (`?dry_run=true`)** — validates a filing-action payload against five preflight checks WITHOUT submitting anything to Altinn / Skatteetaten / NAV. Returns 200 with a `DryRunOutcome` body listing each check's pass/fail; 200 is the SUCCESS path even when individual checks failed (the dry-run itself succeeded — check verdicts are content, not transport).\n\nThe five checks (run in order, all reported, never short-circuited):\n  1. company_exists — does `org_number` exist in Brønnøysundregistrene?\n  2. system_user_authorised — is there an active System User delegation for this consumer + this `org_number`?\n  3. scopes_delegated — does the delegation carry the upstream Maskinporten scopes the action requires? (For mva_melding: `skatteetaten:mva_melding/write`; for a_melding: `altinn:serviceowner/instances.write` AND `nav:aareg/v1/arbeidsforhold`.)\n  4. data_format_valid — does the payload pass the per-action Zod schema?\n  5. deadline_in_future — is the obligation deadline still in the future at clock time?\n\n**Live-execute mode (`?dry_run` omitted or any value other than `true`)** — submits the filing to Altinn 3 (which routes to the Skatteetaten / NAV service owner). v1 supports `mva_melding` only; `a_melding` is gated on the live NAV `aareg/v1/arbeidsforhold` integration and returns 403 PLAN_INSUFFICIENT.\n\nLive-execute applies THREE additional contracts over dry-run:\n  - `Idempotency-Key` header is REQUIRED (a missing key returns 400 IDEMPOTENCY_KEY_REQUIRED — network retries without the same key would double-submit).\n  - `X-Approval-Token` header is REQUIRED. The token is a single-use opaque string minted via `POST /api/v1/auth/approval-token` and consumed atomically via the migration 033 `consume_approval_token_atomic` RPC. Rejected tokens map to APPROVAL_TOKEN_INVALID / APPROVAL_TOKEN_USED / APPROVAL_TOKEN_EXPIRED / APPROVAL_TOKEN_MISMATCH; a NOT_FOUND row collapses to APPROVAL_TOKEN_INVALID for existence-disclosure protection.\n  - The five dry-run preconditions MUST pass. Failures return 422 VALIDATION_FAILED with the failed check list; the approval token is NOT consumed on this path so the agent can fix inputs and retry without burning a token.\n\nSuccessful live-execute returns 200 with a HMAC-SHA256-signed receipt envelope (see `SignedReceipt` schema). The signature is HMAC over `canonicalJSON(receipt-without-signature)`; the hashed domain INCLUDES `signature_algorithm` so a downgrade-attack tag flip fails verification.\n\nDISCRIMINATED PERIOD FORMAT — per `action_type`: mva_melding accepts `YYYY-Tn` (n = 1..6) for bimonthly terminer OR `YYYY-A` for annual filers OR `YYYY-MM` for monthly. a_melding is `YYYY-MM` only.\n\nIDEMPOTENCY — `Idempotency-Key` is OPTIONAL on dry-run (CLAUDE.md Rule 5; PR-073 round 15) and REQUIRED on live-execute (PR-077). When present, the (consumer, endpoint, key) tuple reserves a 24-hour replay slot: a re-send with the same key + same body returns the original response verbatim with `Idempotent-Replay: true`. A re-send with the same key + different body returns 422 IDEMPOTENCY_KEY_MISMATCH; a concurrent in-flight dupe returns 409 IDEMPOTENCY_IN_PROGRESS + Retry-After. PR-IDEMPOTENCY-AUDIT (Amendment 61 §5.4): replays are recorded in audit_log with `was_idempotent_replay=true`. The replay audit row carries the SHA-256 hash of the Idempotency-Key (never the plaintext value) and the endpoint pattern, so forensic queries can distinguish retry storms from genuine duplicate intent and join replay history against `idempotency_keys.key_hash` byte-for-byte.\n\nBODY SIZE — bounded at 256 KB. Beyond that the request returns 413 PAYLOAD_TOO_LARGE.\n\nThe dry-run response body's `outcome.disclaimer` carries the agent contract: a passing dry-run is NOT a guarantee of submission success — government systems may reject otherwise-valid submissions for reasons outside Apier's view (transient outages, server-side validations beyond format).","operationId":"dryRunExecuteAction","tags":["Actions"],"security":[{"BearerAuth":[]}],"x-required-scope":"read:actions","parameters":[{"name":"dry_run","in":"query","required":false,"schema":{"type":"string"},"description":"Mode discriminator (PR-077). When `\"true\"`, the endpoint runs in dry-run mode (validation only, no upstream submission). When omitted or any other value, the endpoint runs in live-execute mode (submission to Altinn / Skatteetaten / NAV). Live-execute requires `Idempotency-Key` AND `X-Approval-Token` headers AND a fully-passing dry-run precondition gate; see the operation description for the full contract."},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":1,"maxLength":255,"pattern":"^[\\x20-\\x7E]+$"},"description":"Idempotency-Key. REQUIRED for live-execute (`?dry_run=true` omitted); a missing or empty value returns 400 IDEMPOTENCY_KEY_REQUIRED — network retries without the same key would double-submit. OPTIONAL on dry-run (`?dry_run=true`) — when present the standard PR-024 dedupe semantics apply (same key + same body replays in 24h with `Idempotent-Replay: true`; same key + different body returns 422 IDEMPOTENCY_KEY_MISMATCH; concurrent in-flight dupe returns 409 IDEMPOTENCY_IN_PROGRESS). The schema mirrors `#/components/parameters/IdempotencyKeyHeader` exactly (1–255 printable ASCII). The header is described inline rather than $ref'd so the dual-mode requirement is documented at the operation level. Round 4 (CodeRabbit Major) — was previously maxLength 256 with no pattern, which widened the contract vs the runtime enforcement; generated SDK clients could ship values the server would 400. PR-IDEMPOTENCY-AUDIT (Amendment 61 §5.4): replays return the cached response and are recorded in audit_log with `was_idempotent_replay=true` — a forensic flag that distinguishes retry storms from genuine duplicate intent. The replay audit row carries `details.idempotency_key_hash` (SHA-256 hex of the raw header value, byte-equal to `idempotency_keys.key_hash` so forensic joins on hash work), `details.endpoint_pattern`, and `details.replay_age_ms`. The raw key VALUE is never persisted — only the hash. Each replay carries its own correlation_id from the current request (not collapsed into the original)."},{"name":"X-Approval-Token","in":"header","required":false,"schema":{"type":"string","minLength":1},"description":"Single-use approval token. REQUIRED for live-execute (PR-077); ignored on dry-run. Mint via `POST /api/v1/auth/approval-token` after a human operator confirms the action.\n\nConsumption semantics (round 9 correction per migration 033 `consume_approval_token_atomic` actual behavior — only the success path runs the CAS UPDATE on `approval_tokens.used_at`):\n\n**Token NOT consumed (retry-safe with same token, within TTL):**\n- `422 VALIDATION_FAILED` — live preconditions failed before the consume RPC ran. Fix the inputs and resubmit with the same token.\n- `403 APPROVAL_TOKEN_EXPIRED` — RPC returned EXPIRED, no UPDATE fired. Token is unchanged (still NULL `used_at`) but unusable due to elapsed TTL. Mint a fresh one — not because the original was burned, but because it's past its expiry.\n- `403 APPROVAL_TOKEN_MISMATCH` (ORG_MISMATCH / ACTION_MISMATCH) — RPC returned mismatch, no UPDATE fired. Token still has NULL `used_at`. The agent COULD theoretically retry with a request matching the token's binding, but practically minting a fresh token bound to the actually-required action_id + org_number is the safer remediation.\n- `500 EXECUTION_FAILED` *PRE-CONSUME* — the consume RPC itself threw an infrastructure error (DB unreachable, RPC contract drift) before reaching the CAS UPDATE. The agent can retry with the same token.\n\n**Token IS consumed (single-use rule — retries require a fresh token):**\n- `200` (success) — happy path, the CAS UPDATE fired and `used_at` is now set.\n- `403 APPROVAL_TOKEN_USED` — a PRIOR successful consume already ran the UPDATE. This response confirms the row is in `used_at IS NOT NULL` state; the original consumer of that token is upstream of this 403.\n- `500 EXECUTION_FAILED` *POST-CONSUME* — a route invariant violation fired after the CAS UPDATE already succeeded. Token row is already updated; the request that invoked the CAS is the one that consumed it.\n- `502 GOVERNMENT_API_ERROR` — submit / sign / persist step failed AFTER the consume CAS succeeded. Single-use-on-failure rule applies — the token is burned by the successful CAS, the downstream failure does NOT roll it back. Mint a fresh token.\n\nThe SDK client cannot distinguish pre-consume from post-consume EXECUTION_FAILED from the response alone — when in doubt, the `submit_mva_started` audit row is the operator-side signal: if it exists for the correlation_id, the consume succeeded and the token is burned. EXPIRED + MISMATCH branches are NEVER consumptive at the RPC layer (round 9 correction — was previously misdescribed as burning the token).\n\nSee DECISIONS.md PR-077 §1 (live-execute gating), §2 (single-use-on-failure rule — applies AFTER successful consume only), and §4 (existence-disclosure protection on NOT_FOUND → APPROVAL_TOKEN_INVALID) for the full contract."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"description":"Discriminated union on `action_type` for the top-level request shape. The payload remains intentionally permissive at the transport layer (`type: object`) so malformed payloads still reach the dry-run engine and surface as `data_format_valid=false` on the documented 200 path. The discriminator mapping points at the full request-body schemas so generated clients can still enforce the top-level `action_type` / `period` / `org_number` contract; per-action payload conformance is reported via the `data_format_valid` check, never as a 400.","oneOf":[{"$ref":"#/components/schemas/DryRunExecuteMvaRequest"},{"$ref":"#/components/schemas/DryRunExecuteAMeldingRequest"}],"discriminator":{"propertyName":"action_type","mapping":{"mva_melding":"#/components/schemas/DryRunExecuteMvaRequest","a_melding":"#/components/schemas/DryRunExecuteAMeldingRequest"}}}}}},"responses":{"200":{"description":"Operation completed. Two discriminated body shapes (PR-077): dry-run path returns `{data: {dry_run: true, outcome: DryRunOutcome}}`; live-execute path returns `{data: {dry_run: false, altinn_receipt_id, receipt_id, submitted_at, signed_receipt: SignedReceipt}}`. Discriminate on `data.dry_run`.","headers":{"Link":{"$ref":"#/components/headers/LinkServiceDescriptorHeader"},"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimitHeader"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemainingHeader"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitResetHeader"},"Idempotent-Replay":{"description":"Set by the Idempotency-Key middleware. `true` when the response was replayed from a prior request that supplied the same Idempotency-Key + matching body (the cached response is byte-identical to the original; the route handler does NOT run, so NO fresh `evaluation_snapshots` row is written). `false` when the handler ran fresh on this call. Absent when the request did not carry an Idempotency-Key header. PR-073 round 16 (CodeRabbit Major). PR-IDEMPOTENCY-AUDIT (Amendment 61 §5.4): a replay STILL writes one `audit_log` row from the middleware tagged `was_idempotent_replay=true` — so retry storms are distinguishable from genuine duplicate intent in forensics. The replay audit row carries the SHA-256 hash of the Idempotency-Key (NEVER the plaintext value); see the `Idempotency-Key` parameter description for the full forensic schema.","schema":{"type":"string","enum":["true","false"]}},"Idempotent-Replay-Age-Ms":{"description":"Set alongside `Idempotent-Replay: true`. Milliseconds elapsed between the original request that produced the cached response and this replay request. Useful for diagnosing whether a client is replaying within or near the 24h reservation TTL.","schema":{"type":"integer","minimum":0}},"X-Apier-Upstream-Healthy":{"$ref":"#/components/headers/ApierUpstreamHealthyHeader","description":"PR-A round 3 — the dry-run branch (`data.dry_run === true`) does NOT carry this header: dry-run skips the reliability orchestrator entirely (no upstream call, no breaker read), so the route emits no `X-Apier-Upstream-Healthy` for that branch. The live-execute branch (`data.dry_run === false`) always carries the header. SDK clients should treat the header as branch-conditional within this 200 response, NOT mandatory."}},"content":{"application/json":{"schema":{"type":"object","required":["success","data","_meta"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"description":"Discriminated by the `dry_run` property's boolean literal — `true` indicates the dry-run branch (DryRunSuccessData), `false` indicates the live-execute branch (ExecuteSuccessData). Each subschema pins `dry_run` to a single-value enum so SDK generators that don't honour OpenAPI's optional `discriminator` keyword can still discriminate at runtime by reading `data.dry_run`. Round 6 (Cursor BugBot Medium) — the explicit `discriminator` block was removed because OpenAPI 3.1 requires the discriminator's property to be a string at runtime, but `dry_run` is boolean; mapping keys `\"true\"`/`\"false\"` would never match the JSON values `true`/`false`. The literal-enum approach is spec-compliant AND semantically equivalent to a discriminator for this use case.","oneOf":[{"$ref":"#/components/schemas/DryRunSuccessData"},{"$ref":"#/components/schemas/ExecuteSuccessData"}]},"_meta":{"$ref":"#/components/schemas/TrustMetadata"}}}}}},"400":{"description":"Transport-level validation failed before the engine ran. Possible `error_code` values: `VALIDATION_FAILED` (malformed JSON, top-level body validation failure such as missing `action_type` / invalid `period` format / non-object `payload`, or malformed `Idempotency-Key` header value); `IDEMPOTENCY_KEY_REQUIRED` (PR-077 — live-execute requires the `Idempotency-Key` header; dry-run does not). Per-action payload validation never surfaces here on the dry-run path — those failures arrive on 200 with `data_format_valid=false`.","headers":{"Link":{"$ref":"#/components/headers/LinkServiceDescriptorHeader"},"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimitHeader"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemainingHeader"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitResetHeader"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Authorization or scope failure. Possible `error_code` values: `SCOPE_INSUFFICIENT` / `SCOPE_RESERVED` (API key lacks the `read:actions` scope or holds a reserved-prefix scope); `PLAN_INSUFFICIENT` (PR-077 live-execute — the action_type is not yet available on the live path; v1 supports `mva_melding` only); `APPROVAL_TOKEN_REQUIRED` (PR-077 live-execute — `X-Approval-Token` header missing); `APPROVAL_TOKEN_INVALID` (token not recognised — also returned for tokens that never existed, existence-disclosure protection); `APPROVAL_TOKEN_USED` (single-use token already consumed); `APPROVAL_TOKEN_EXPIRED` (token TTL elapsed); `APPROVAL_TOKEN_MISMATCH` (token bound to a different `org_number` or `action_id` — the route maps both `mva_melding` and `a_melding` action_types to the canonical `filing.submit` action_id at consume time via `actionTypeToActionId`; mismatches surface against the action_id binding, not the surface-level action_type).","headers":{"Link":{"$ref":"#/components/headers/LinkServiceDescriptorHeader"},"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimitHeader"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemainingHeader"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitResetHeader"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"409":{"description":"Two discriminated conflict scenarios — `error_code` is the discriminator. `IDEMPOTENCY_IN_PROGRESS`: another request with the same Idempotency-Key is still mid-flight (PR-024). `CONCURRENT_WRITE_IN_FLIGHT`: PR-AGENT-CONFLICT-LOCK (Amendment 61 §5.1) cross-agent write-conflict gate fired — body carries `error: \"concurrent_write_in_flight\"` (spec literal) + `first_correlation_id` + `retry_after_seconds`. **Branch-specific response header (NOT advertised at the union level because it is variant-specific):** the `CONCURRENT_WRITE_IN_FLIGHT` branch emits `X-Apier-Skip-Idempotency-Cache: 1`; the `IDEMPOTENCY_IN_PROGRESS` branch does NOT. See `#/components/responses/Conflict409` for the dedicated variant-shape with the header documented.","headers":{"Link":{"$ref":"#/components/headers/LinkServiceDescriptorHeader"},"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimitHeader"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemainingHeader"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitResetHeader"},"Retry-After":{"description":"Seconds to wait before retrying. On `CONCURRENT_WRITE_IN_FLIGHT` this is the conflict-lock TTL remainder (always >= 1, server clamps via GREATEST(1, CEIL(...))). On `IDEMPOTENCY_IN_PROGRESS` this is the in-flight wait hint.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/IdempotencyInProgressBody"},{"$ref":"#/components/schemas/Conflict409Body"}],"discriminator":{"propertyName":"error_code","mapping":{"IDEMPOTENCY_IN_PROGRESS":"#/components/schemas/IdempotencyInProgressBody","CONCURRENT_WRITE_IN_FLIGHT":"#/components/schemas/Conflict409Body"}}}}}},"413":{"description":"Request body exceeds 256 KB.","headers":{"Link":{"$ref":"#/components/headers/LinkServiceDescriptorHeader"},"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimitHeader"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemainingHeader"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitResetHeader"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"422":{"description":"Two discriminated paths (PR-077): `IDEMPOTENCY_KEY_MISMATCH` (same Idempotency-Key + different body — the original response is preserved; the wrapper rejects the conflict per RFC draft); `VALIDATION_FAILED` on live-execute when one or more dry-run preconditions failed (the live path runs the same five preflight checks as a precondition gate; failures are reported with `details` listing the failed check names). On the precondition-failure path the approval token is NOT consumed — the agent can fix inputs and retry without burning a token.\n\nRound 5 (CodeRabbit Major) — schema is now a oneOf so SDK clients can distinguish the two contracts. `ApiError` covers IDEMPOTENCY_KEY_MISMATCH (closed `error_code` enum from the central component); `ExecutePreconditionFailedError` covers the precondition-failure case with a typed `details` array of `{field, message}` entries naming the failed checks. The discriminator is `error_code`.","headers":{"Link":{"$ref":"#/components/headers/LinkServiceDescriptorHeader"},"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimitHeader"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemainingHeader"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitResetHeader"}},"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/IdempotencyMismatchError"},{"$ref":"#/components/schemas/ExecutePreconditionFailedError"}],"discriminator":{"propertyName":"error_code","mapping":{"IDEMPOTENCY_KEY_MISMATCH":"#/components/schemas/IdempotencyMismatchError","VALIDATION_FAILED":"#/components/schemas/ExecutePreconditionFailedError"}}}}}},"429":{"$ref":"#/components/responses/RateLimitExceeded"},"500":{"description":"Internal server-side error. Possible `error_code` values: `DRY_RUN_FAILED` (the dry-run engine threw despite its non-throwing contract); `INTERNAL_ERROR` (a runtime invariant violation — e.g. handlePost reached without the outer body-bound wrap); `INTERNAL_SERVER_ERROR` (outer body-bound helper threw an unexpected non-typed error); `EXECUTION_FAILED` (PR-077 — TWO emit-sites: (a) PRE-CONSUME: approval-token consume RPC threw an infrastructure error before the CAS update fired; the approval token is NOT consumed and the caller can retry with the same token. (b) POST-CONSUME: a route invariant violation fired after the CAS already succeeded; the approval token IS consumed and a fresh token is required to retry. The response body alone does not distinguish (a) from (b); when in doubt, mint a fresh token. See DECISIONS.md PR-077 §2 single-use-on-failure rule for the full contract). All captured to Sentry with the correlation_id; clients should retry with the same correlation_id (and same Idempotency-Key if used — the retry is replay-safe) and contact support if persistent.","headers":{"Link":{"$ref":"#/components/headers/LinkServiceDescriptorHeader"},"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimitHeader"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemainingHeader"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitResetHeader"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"502":{"description":"PR-077 live-execute — upstream submission failed. Altinn / Skatteetaten / NAV rejected the submission, OR the receipt signing or persistence step threw after a successful upstream call. The approval token IS consumed in this path (single-use-on-failure rule). Clients must mint a fresh approval token before retrying. The audit chain records the failure with `action: submit_mva_failed` and the error class.\n\nPR-A round 3 — `error_code` is the orchestrator's stable apier_code, NOT a fixed `GOVERNMENT_API_ERROR`. Possible values per `ApierErrorCode` enum: `GOVERNMENT_API_ERROR` (default), `GOVERNMENT_RATE_LIMITED`, `GOVERNMENT_UNAVAILABLE`, `GOVERNMENT_VALIDATION_REJECTED`, `MASKINPORTEN_AUTH_FAILED`, `ALTINN_DELEGATION_MISSING`, `EXECUTION_DEADLINE_EXCEEDED`, `EXECUTION_CIRCUIT_OPEN`, `RETRY_BUDGET_EXHAUSTED`, `EXECUTION_FAILED` (round 29 — post-submit local-failure: Altinn submit succeeded but signReceipt/persistReceipt threw; route emits this code so SDK clients route to reconcile-don't-retry guidance). SDK clients can route on these distinct codes for distinct remediation paths.\n\nPR-A round 5 — body is `ExecuteReliabilityError` (extends `ApiErrorWithStrictProvenance` with `execution_guarantee` + `outcome`). The orchestrator-derived reliability metadata is now emitted on the failure path, matching the success envelope's PR-A contract.\n\nPR-A round 2 — `X-Apier-Upstream-Healthy` is set on this response because the orchestrator ran and produced an authoritative breaker snapshot before the failure surfaced (the breaker may be CLOSED with a transient upstream rejection, or OPEN/HALF_OPEN if cumulative failures crossed the threshold). The header lets the agent gate retries on real-time breaker state without waiting for the next call.","headers":{"Link":{"$ref":"#/components/headers/LinkServiceDescriptorHeader"},"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimitHeader"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemainingHeader"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitResetHeader"},"X-Apier-Upstream-Healthy":{"$ref":"#/components/headers/ApierUpstreamHealthyHeader"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExecuteReliabilityError"}}}},"503":{"description":"Three discriminated `error_code` values for this status — `error_code` is the discriminator. Round-5 fix (CodeRabbit Minor) — the schemas DIFFER per branch: the `oneOf` below maps each `error_code` to a distinct schema component. Earlier copy claimed all three shared one envelope; that was correct before round-2 narrowed the legacy variants and added the conflict-lock SAFETY_SYSTEM_UNAVAILABLE shape with extra fields (`error`, `execution_guarantee`, `outcome`):\n\n- `UPSTREAM_UNAVAILABLE` — Idempotency reservation service is temporarily unavailable. Emitted by the `withIdempotency` wrapper when the underlying Supabase RPC for the reservation fails. Fail-CLOSED behaviour, the handler is NOT executed (a duplicate write is strictly worse than a 503; the caller can retry). Clients using `Idempotency-Key` should retry with the SAME key. (PR-073 round 20.)\n\n- `ALTINN_LIVE_SUBMITTER_NOT_IMPLEMENTED` — Server-side rollout misconfiguration: emitted by the `getAltinnSubmitter` adapter selector when `ALTINN_MODE=live` is set in the OPERATOR's environment but the live submitter implementation hasn't been wired (`src/lib/altinn/live-submitter.ts` slot reserved). Fail-CLOSED at boot rather than silently submitting via the mock. The route catches the AltinnError thrown by the selector and forwards its structured envelope (summary, why, fix_steps in Norwegian bokmål). API CONSUMERS cannot fix this — `ALTINN_MODE` is a server env var, not a request header — so the remediation is operator-side: contact Apier support with the X-Correlation-ID, OR retry later once the activation gate clears (the operator will have flipped `ALTINN_MODE=mock` until the live submitter ships per DECISIONS.md \"PR-077 Live Execute Gating\"). Until that happens, the agent should treat this as a transient outage and back off + retry.\n\n- `SAFETY_SYSTEM_UNAVAILABLE` — Amendment 61 §5.1 conflict-lock acquire RPC failed. Fail-CLOSED for safety: serving the request without verifying that no other agent is mid-write would risk dual-submission. **This is the only branch that emits `X-Apier-Skip-Idempotency-Cache: 1`** (UPSTREAM_UNAVAILABLE and ALTINN_LIVE_SUBMITTER_NOT_IMPLEMENTED do NOT). See #/components/responses/SafetyUnavailable503 for the dedicated variant shape with the header documented.\n\nClients MUST branch on `error_code` from the body to select the correct generated SDK type — the three branches have different structural shapes per the `oneOf` discriminator.","headers":{"Link":{"$ref":"#/components/headers/LinkServiceDescriptorHeader"},"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimitHeader"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemainingHeader"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitResetHeader"}},"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/UpstreamUnavailableBody"},{"$ref":"#/components/schemas/AltinnLiveSubmitterNotImplementedBody"},{"$ref":"#/components/schemas/SafetyUnavailable503Body"}],"discriminator":{"propertyName":"error_code","mapping":{"UPSTREAM_UNAVAILABLE":"#/components/schemas/UpstreamUnavailableBody","ALTINN_LIVE_SUBMITTER_NOT_IMPLEMENTED":"#/components/schemas/AltinnLiveSubmitterNotImplementedBody","SAFETY_SYSTEM_UNAVAILABLE":"#/components/schemas/SafetyUnavailable503Body"}}}}}}}}},"/api/v1/account/signup":{"parameters":[{"$ref":"#/components/parameters/CorrelationIdHeader"}],"post":{"summary":"Request a magic-link signup","description":"Consumer self-serve sign-up entry point. Accepts an email (required) and an optional company_name, requests a Supabase Auth magic link, and returns a constant-time `{ok: true}` response on every valid Zod input — regardless of whether the email is already registered. The email-enumeration defense is intentional: returning a different shape for new vs existing emails would let attackers probe the registered-email set. The login flow MUST omit company_name to avoid clobbering returning-user metadata; the signup flow sends it. Rate-limited at 5 requests per hour per IP, fail-CLOSED (a rate-limit-store outage blocks signups rather than widening the gate). Returns 400 only on Zod validation failure (malformed email, or company_name present but empty / oversize); 429 when the limit is hit.","operationId":"accountSignup","tags":["Account"],"security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["email"],"properties":{"email":{"type":"string","format":"email","maxLength":320,"description":"Email address to send the magic link to. Re-used to provision the api_consumers row on first sign-in."},"company_name":{"type":"string","minLength":1,"maxLength":200,"description":"Optional. Display name for the consumer, surfaced on the dashboard. Sign-up form sends a real value; the login form omits it so a returning user's user_metadata.company_name is preserved across re-authentications."}}}}}},"responses":{"200":{"description":"Magic link request accepted. The same response is returned whether the email was new or already registered. The X-Correlation-ID response header echoes the request's correlation id (or a freshly-minted one) so the caller can thread it into their own trace system. The Link header carries `</openapi.json>; rel=\"service-desc\"` per Rule 4.","headers":{"X-Correlation-ID":{"schema":{"type":"string","format":"uuid"},"description":"Echoed back from the request, or a fresh UUID v4 minted by middleware when the request lacked a valid one. Persisted on every audit_log row produced by this request."},"Link":{"schema":{"type":"string"},"description":"Always `</openapi.json>; rel=\"service-desc\"` (Rule 4). Lets agents discover the OpenAPI spec from any /api/v1/ response."}},"content":{"application/json":{"schema":{"type":"object","required":["success","data","_meta"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean","enum":[true]}}},"_meta":{"$ref":"#/components/schemas/TrustMetadata"}}}}}},"400":{"description":"Validation failed — malformed JSON, malformed email, or empty/oversize company_name (when company_name is provided it must be 1–200 chars; omit it entirely on the login flow to avoid clobbering returning-user metadata). X-Correlation-ID + Link headers carry the same correlation/discovery semantics as the 200 response.","headers":{"X-Correlation-ID":{"schema":{"type":"string","format":"uuid"},"description":"Echoed back from the request, or a fresh UUID v4 minted by middleware when the request lacked a valid one."},"Link":{"schema":{"type":"string"},"description":"Always `</openapi.json>; rel=\"service-desc\"` (Rule 4)."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"Rate limit exceeded (5 requests per hour per IP, fail-CLOSED) or rate-limit store unavailable. Try again after the hour window resets. Implementations SHOULD honour the Retry-After header when present.","headers":{"X-Correlation-ID":{"schema":{"type":"string","format":"uuid"},"description":"Echoed back from the request, or a fresh UUID v4 minted by middleware when the request lacked a valid one."},"Link":{"schema":{"type":"string"},"description":"Always `</openapi.json>; rel=\"service-desc\"` (Rule 4)."},"Retry-After":{"schema":{"type":"integer","minimum":1},"description":"Seconds until the current rate-limit window closes. May be omitted when the underlying store cannot determine the remaining window (fail-CLOSED store-error path)."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/account/me":{"get":{"operationId":"consumerMe","tags":["Account"],"summary":"Consumer self-service identity","description":"Returns the signed-in consumer's identity surface (email, company_name) for the /dashboard header. Session-cookie auth via @supabase/ssr — NOT Bearer API key auth. This surface is browser-facing only; AI agents using programmatic Bearer-keyed access should NOT call this endpoint and it is deliberately omitted from /v1/capabilities and llms.txt. `tier` is deliberately NOT shipped in v1 — Rule 10 makes response fields append-only, and shipping a placeholder 'free' today that later flips to a real tier value would change the semantics of an existing field (would require /v2). When Starter/Pro surfaces ship and the api_consumers.tier value can be authoritative, `tier` lands as an ADDITIVE field — not a placeholder-to-truth swap.","security":[{"SessionCookieAuth":[]}],"responses":{"200":{"description":"Consumer identity payload.","content":{"application/json":{"schema":{"type":"object","required":["success","data","_meta"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["consumer_id","email","company_name"],"properties":{"consumer_id":{"type":"string","format":"uuid"},"email":{"type":"string","format":"email"},"company_name":{"type":["string","null"]}}},"_meta":{"$ref":"#/components/schemas/TrustMetadata"}}}}}},"401":{"description":"No session cookie or session resolution failed. Browser caller should redirect to /login.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/account/keys":{"get":{"operationId":"consumerKeysList","tags":["Account"],"summary":"List consumer's active API keys","description":"Returns the consumer's active (non-revoked) API keys as a JSON array. Response shape deliberately omits key_hash (NEVER leaves the server) and the plaintext key (returned only ONCE in the POST 201 response). Session-cookie auth — see /api/v1/account/me for the auth-model rationale. Cross-tenant isolation enforced by the route handler's explicit `.eq('consumer_id', $1)` filter AS WELL AS RLS on api_keys.","security":[{"SessionCookieAuth":[]}],"responses":{"200":{"description":"Active keys for the signed-in consumer.","content":{"application/json":{"schema":{"type":"object","required":["success","data","_meta"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["keys","overflow"],"properties":{"keys":{"type":"array","items":{"type":"object","required":["id","name","scopes","last_used_at","created_at","is_revoked"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":["string","null"]},"scopes":{"type":"array","items":{"type":"string"}},"last_used_at":{"type":["string","null"],"format":"date-time"},"created_at":{"type":"string","format":"date-time"},"is_revoked":{"type":"boolean","enum":[false]}}}},"overflow":{"type":"boolean","description":"True when the consumer has more than 50 active keys (defensive max-3-invariant tripwire — in normal operation always false). Generated SDKs should surface a 'refresh / contact support' message rather than render an incomplete list as final."}}},"_meta":{"$ref":"#/components/schemas/TrustMetadata"}}}}}},"401":{"description":"No session cookie.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Database error during list.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"post":{"operationId":"consumerKeysCreate","tags":["Account"],"summary":"Mint a new API key for the signed-in consumer","description":"Generates a fresh API key for the signed-in consumer. **The plaintext_key field is returned EXACTLY ONCE in the 201 body — record it before leaving the page; the server only persists the SHA-256 hash and cannot reissue.** Atomic max-3-active-keys-per-consumer enforcement via the create_consumer_key RPC's pg_advisory_xact_lock; concurrent POSTs race correctly. Inline rate-limit at 10/hour per consumer, fail-CLOSED. Session-cookie auth — NOT Bearer. NEVER wrapped with withIdempotency: caching the response body would keep plaintext recoverable from the DB for 24h, defeating Rule 15.","security":[{"SessionCookieAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":80,"description":"Human-readable label shown in the /dashboard keys table. e.g. 'Local dev', 'CI pipeline'."}}}}}},"responses":{"201":{"description":"Key minted. plaintext_key is the ONLY copy the consumer will ever see. Response carries the standard X-Correlation-ID + Link tracing headers (applied by proxy.ts to every /api/v1/* response) AND the full no-cache directive set `Cache-Control: no-store, private, max-age=0, must-revalidate` + `Pragma: no-cache` + `Expires: 0` — defense in depth so no CDN / SW / proxy ever caches the plaintext body.","headers":{"Cache-Control":{"schema":{"type":"string","enum":["no-store, private, max-age=0, must-revalidate"]},"description":"Defense in depth — plaintext_key is in the body and must NEVER be cached."},"Pragma":{"schema":{"type":"string","enum":["no-cache"]}},"Expires":{"schema":{"type":"string","enum":["0"]}}},"content":{"application/json":{"schema":{"type":"object","required":["success","data","_meta"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["id","name","scopes","created_at","plaintext_key"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"scopes":{"type":"array","items":{"type":"string"}},"created_at":{"type":"string","format":"date-time"},"plaintext_key":{"type":"string","pattern":"^apr_[a-z]+_[A-Za-z0-9_-]{32}$","description":"ONLY returned here. Never persisted. Never reissued."}}},"_meta":{"$ref":"#/components/schemas/TrustMetadata"}}}}}},"400":{"description":"Body failed Zod validation (missing/oversize name).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"No session cookie.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"409":{"description":"MAX_KEYS_REACHED — consumer already has 3 active keys. fix_steps in explanation guide the user.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"RATE_LIMITED — more than 10 create attempts within the rolling hour for this consumer. Retry-After header carries the seconds-until-reset.","headers":{"Retry-After":{"schema":{"type":"integer","minimum":1},"description":"Seconds the client should wait before retrying (RFC 9110 §10.2.3)."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"RPC failure or unexpected DB error. No raw error detail leaks to the response (Rule 24).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/account/keys/{id}":{"delete":{"operationId":"consumerKeysRevoke","tags":["Account"],"summary":"Revoke a specific consumer-owned API key","description":"Soft-revokes (is_revoked=true + revoked_at=now() atomically — both columns must move together to satisfy the chk_revoked_consistency CHECK from migration 001 and the consumer_revoke_own_keys WITH CHECK from PR-SIGNUP migration 20260510074126). Idempotent: revoking an already-revoked key returns 404. Cross-tenant isolation: explicit `.eq('consumer_id', $1)` filter AS WELL AS RLS. Session-cookie auth. **The 404 response deliberately collapses three cases — wrong owner, already revoked, id doesn't exist — so an attacker cannot enumerate the consumer-key space across tenants (IDOR defense).** Inline rate-limit 30/hour per consumer, fail-CLOSED.","security":[{"SessionCookieAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Key id (UUID). 400 if not a valid UUID — the boundary Zod parse rejects before any DB call."}],"responses":{"200":{"description":"Key revoked.","content":{"application/json":{"schema":{"type":"object","required":["success","data","_meta"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["id","revoked_at"],"properties":{"id":{"type":"string","format":"uuid"},"revoked_at":{"type":"string","format":"date-time"}}},"_meta":{"$ref":"#/components/schemas/TrustMetadata"}}}}}},"400":{"description":"Path id is not a valid UUID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"No session cookie.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"KEY_NOT_FOUND — collapses wrong-owner + already-revoked + id-doesn't-exist for IDOR defense.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"description":"RATE_LIMITED — more than 30 revoke attempts within the rolling hour for this consumer. Retry-After header carries the seconds-until-reset.","headers":{"Retry-After":{"schema":{"type":"integer","minimum":1},"description":"Seconds the client should wait before retrying (RFC 9110 §10.2.3)."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected DB error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/account/usage":{"get":{"operationId":"consumerUsage","tags":["Account"],"summary":"30-day usage aggregation for the dashboard chart","description":"Returns exactly 30 contiguous { day, calls } buckets for the signed-in consumer, bucketed by Europe/Oslo calendar day (DST-safe via Intl.DateTimeFormat so spring-forward and fall-back transitions produce neither skipped nor duplicate labels). Server-side day-fill: days with no traffic are returned as `calls: 0`, never omitted, so the recharts line stays continuous. Session-cookie auth. Cross-tenant defense: explicit `.eq('consumer_id', $1)`.","security":[{"SessionCookieAuth":[]}],"responses":{"200":{"description":"30 contiguous Oslo-local day buckets + total + window size.","content":{"application/json":{"schema":{"type":"object","required":["success","data","_meta"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["days","total","window_days"],"properties":{"days":{"type":"array","minItems":30,"maxItems":30,"items":{"type":"object","required":["day","calls"],"properties":{"day":{"type":"string","pattern":"^[0-9]{4}-[0-9]{2}-[0-9]{2}$"},"calls":{"type":"integer","minimum":0}}}},"total":{"type":"integer","minimum":0},"window_days":{"type":"integer","enum":[30]}}},"_meta":{"$ref":"#/components/schemas/TrustMetadata"}}}}}},"401":{"description":"No session cookie.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"500":{"description":"Unexpected DB error during aggregation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/admin/keys":{"parameters":[{"$ref":"#/components/parameters/CorrelationIdHeader"}],"post":{"summary":"Create a new API key","description":"Generates a new API key for the specified consumer. The plaintext key is returned ONCE in the response — after this, only the SHA-256 hash exists in the database. Maximum 3 active keys per consumer. This operation is recorded in the append-only audit log.","operationId":"createApiKey","tags":["Admin"],"x-admin":true,"security":[{"AdminBearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["consumer_id"],"properties":{"consumer_id":{"type":"string","format":"uuid","description":"The consumer to create a key for."},"label":{"type":"string","description":"Optional human-readable label for the key."}}}}}},"responses":{"201":{"description":"API key created. The plaintext key is included ONCE.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[true],"description":"Always true on success."},"data":{"type":"object","properties":{"key":{"type":"string","description":"The plaintext API key. Store securely — shown only once."},"id":{"type":"string","format":"uuid","description":"Key identifier."},"label":{"type":"string","nullable":true,"description":"Human-readable label."},"created_at":{"type":"string","format":"date-time","description":"When the key was created."}}},"_meta":{"$ref":"#/components/schemas/TrustMetadata"}}}}}},"400":{"description":"Validation failed — invalid consumer_id or label.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Consumer not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"409":{"description":"Maximum 3 active keys per consumer.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"key_limit":{"summary":"Consumer already has 3 active keys","value":{"success":false,"error_code":"KEY_LIMIT_REACHED","explanation":{"summary":"Maximum 3 active API keys per consumer"},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-16T12:00:00.000Z","last_verified":"2026-04-16T12:00:00.000Z","source":"apier.no","schema_version":"1.0.5","response_timestamp":"2026-04-25T12:00:00.000Z","response_hash":"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}}}}}}}}},"get":{"summary":"List active API keys for a consumer","description":"Returns all non-revoked keys for the specified consumer. The plaintext key is never returned — only id, label, created_at, and last_used_at.","operationId":"listApiKeys","tags":["Admin"],"x-admin":true,"security":[{"AdminBearerAuth":[]}],"parameters":[{"name":"consumer_id","in":"query","required":true,"schema":{"type":"string","format":"uuid"},"description":"The consumer whose keys to list."}],"responses":{"200":{"description":"List of active API keys (never includes plaintext key).","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[true],"description":"Always true on success."},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Key identifier."},"label":{"type":"string","nullable":true,"description":"Human-readable label."},"created_at":{"type":"string","format":"date-time","description":"When the key was created."},"last_used_at":{"type":"string","format":"date-time","nullable":true,"description":"When the key was last used for authentication."}}}},"_meta":{"$ref":"#/components/schemas/TrustMetadata"}}}}}},"400":{"description":"Validation failed — missing or invalid consumer_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/admin/keys/{id}":{"parameters":[{"$ref":"#/components/parameters/CorrelationIdHeader"}],"delete":{"summary":"Revoke an API key","description":"Soft-revokes an API key by setting is_revoked=true and revoked_at=now(). The key can no longer be used for authentication. This action is irreversible. This operation is recorded in the append-only audit log. Note: Idempotency-Key support planned for a future release.","operationId":"revokeApiKey","tags":["Admin"],"x-admin":true,"security":[{"AdminBearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"The key ID to revoke."}],"responses":{"200":{"description":"Key successfully revoked.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[true],"description":"Always true on success."},"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"The revoked key ID."},"revoked":{"type":"boolean","enum":[true],"description":"Always true after revocation."}}},"_meta":{"$ref":"#/components/schemas/TrustMetadata"}}}}}},"400":{"description":"Validation failed — malformed UUID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Key not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"409":{"description":"Key already revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/auth/system-user/delegate":{"parameters":[{"$ref":"#/components/parameters/CorrelationIdHeader"}],"post":{"summary":"Create an Altinn 3 System User delegation","description":"Auth Gateway core flow. The consumer requests an Altinn System User to act on behalf of a Norwegian company (org_number) with a specific set of scopes. Altinn returns a delegation URL that the company's signing authority (signaturrett) must visit to approve. Until approval the delegation remains in 'pending' status; once approved it transitions to 'active' and the system user can be used to call Altinn on the organisation's behalf. The integration can run in live or sandbox/mock mode depending on the deployment's configuration. This operation is recorded in the append-only audit log.","operationId":"createSystemUserDelegation","x-required-scope":"delegate:*","tags":["Auth Gateway"],"security":[{"BearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKeyHeader"},{"$ref":"#/components/parameters/AgentVendorHeader"},{"$ref":"#/components/parameters/AgentModelHeader"},{"$ref":"#/components/parameters/AgentRunIdHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["org_number","scopes"],"properties":{"org_number":{"type":"string","pattern":"^[0-9]{9}$","description":"Norwegian organisation number. Exactly 9 digits, matching Brønnøysund Register Centre format. Any other shape returns 400 VALIDATION_FAILED before the upstream call is made.","example":"123456789"},"scopes":{"type":"array","minItems":1,"items":{"type":"string","minLength":1},"description":"Altinn scopes to request (e.g. 'altinn:accessmanagement/authorizedparties.read'). At least one scope is required. The scope list appears verbatim on the approval page shown to the signing authority, so request only what the integration actually needs.","example":["altinn:accessmanagement/authorizedparties.read"]},"validity_days":{"type":"integer","minimum":1,"maximum":3650,"description":"Desired delegation validity window in days. Optional — if omitted, Altinn applies its own default cap. Upper bound of 10 years matches Altinn's longest supported delegation term.","example":365},"label":{"type":"string","maxLength":255,"description":"Consumer-supplied label shown in the dashboard to distinguish multiple delegations for the same org/consumer pair. Never forwarded to Altinn.","example":"Production integration — Oslo HQ"}}}}}},"responses":{"201":{"description":"Delegation created. Until the org's signing authority approves the delegation_url, status remains 'pending' and the system user cannot yet be used to call Altinn on the org's behalf.","content":{"application/json":{"schema":{"type":"object","required":["success","data","_meta"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["system_user_id","status","scopes","valid_until","delegation_url","label"],"properties":{"system_user_id":{"type":"string","description":"Altinn-issued System User identifier. Use this to reference the delegation in later Altinn calls.","example":"altinn-su-12345"},"status":{"type":"string","enum":["pending","active","expired","revoked"],"description":"Current lifecycle state. New delegations always start as 'pending' until the signing authority approves."},"scopes":{"type":"array","items":{"type":"string"},"description":"Scopes Altinn recorded for this delegation. May be a subset of the requested list if any were rejected."},"valid_until":{"type":"string","format":"date-time","nullable":true,"description":"ISO 8601 UTC timestamp when the delegation expires. Null for indefinite."},"delegation_url":{"type":"string","format":"uri","description":"Approval URL. The organisation's signing authority must visit this URL and approve the delegation before the system user becomes active."},"label":{"type":"string","nullable":true,"description":"Echo of the consumer-supplied label from the request (null if the caller omitted it). Stored server-side for dashboard display and returned here so clients have a symmetric contract without needing a follow-up GET."}}},"_meta":{"$ref":"#/components/schemas/TrustMetadata"}}},"examples":{"pending_delegation":{"summary":"Freshly created delegation awaiting approval","value":{"success":true,"data":{"system_user_id":"mock_su_11111111-1111-1111-1111-111111111111","status":"pending","scopes":["altinn:accessmanagement/authorizedparties.read"],"valid_until":"2027-04-17T10:00:00.000Z","delegation_url":"https://mock.altinn.no/delegate/11111111-1111-1111-1111-111111111111","label":"Production integration — Oslo HQ"},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-17T10:00:00.000Z","last_verified":"2026-04-17T10:00:00.000Z","source":"apier.no","schema_version":"1.0.5","response_timestamp":"2026-04-25T12:00:00.000Z","response_hash":"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}}}}}}},"400":{"description":"Request validation failed or Altinn rejected the requested scopes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"invalid_org_number":{"summary":"org_number is not 9 digits","value":{"success":false,"error_code":"VALIDATION_FAILED","explanation":{"summary":"Invalid request parameters","details":[{"field":"org_number","message":"Må være nøyaktig 9 siffer (eksempel: 123456789)"}]},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-17T10:00:00.000Z","last_verified":"2026-04-17T10:00:00.000Z","source":"apier.no","schema_version":"1.0.5","response_timestamp":"2026-04-25T12:00:00.000Z","response_hash":"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}}},"scope_missing":{"summary":"Altinn rejected one or more requested scopes","value":{"success":false,"error_code":"SCOPE_MISSING","explanation":{"summary":"Altinn rejected the requested scopes","why":"Ett eller flere av de forespurte scopes er ikke tilgjengelige for denne organisasjonen.","fix_steps":["Kontroller scope-listen mot Altinns systemregister-katalog"]},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-17T10:00:00.000Z","last_verified":"2026-04-17T10:00:00.000Z","source":"apier.no","schema_version":"1.0.5","response_timestamp":"2026-04-25T12:00:00.000Z","response_hash":"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Altinn rejected the delegation because the caller lacks the authorising rights for this organisation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"no_delegation":{"summary":"Caller is not authorised for this organisation","value":{"success":false,"error_code":"AUTH_NO_DELEGATION","explanation":{"summary":"Altinn rejected the delegation request","why":"Innringer mangler rettighetene som trengs for å opprette systembruker for denne organisasjonen.","fix_steps":["Kontroller at Maskinporten-klienten har riktige scopes","Bekreft at organisasjonen har autorisert API-forbrukeren"],"legal_basis":"Forvaltningsloven § 12"},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-17T10:00:00.000Z","last_verified":"2026-04-17T10:00:00.000Z","source":"apier.no","schema_version":"1.0.5","response_timestamp":"2026-04-25T12:00:00.000Z","response_hash":"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}}}}}}},"409":{"description":"Two discriminated conflict scenarios — `error_code` is the discriminator. `IDEMPOTENCY_IN_PROGRESS`: another request with the same Idempotency-Key is still mid-flight. `CONCURRENT_WRITE_IN_FLIGHT`: PR-AGENT-CONFLICT-LOCK (Amendment 61 §5.1) cross-agent write-conflict gate fired — body carries `error: \"concurrent_write_in_flight\"` + `first_correlation_id` + `retry_after_seconds`. **Branch-specific response header (NOT advertised at the union level because it is variant-specific):** the `CONCURRENT_WRITE_IN_FLIGHT` branch emits `X-Apier-Skip-Idempotency-Cache: 1`; the `IDEMPOTENCY_IN_PROGRESS` branch does NOT. See `#/components/responses/Conflict409` for the dedicated variant shape.","headers":{"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"$ref":"#/components/headers/LinkServiceDescriptorHeader"},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/IdempotencyInProgressBody"},{"$ref":"#/components/schemas/Conflict409Body"}],"discriminator":{"propertyName":"error_code","mapping":{"IDEMPOTENCY_IN_PROGRESS":"#/components/schemas/IdempotencyInProgressBody","CONCURRENT_WRITE_IN_FLIGHT":"#/components/schemas/Conflict409Body"}}}}}},"422":{"$ref":"#/components/responses/IdempotencyMismatch"},"429":{"$ref":"#/components/responses/RateLimitExceeded"},"500":{"description":"Unexpected upstream adapter failure. Internal details are never leaked in the response body; clients should retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"502":{"description":"Altinn upstream failure (unreachable, malformed response, server access token rejected, token acquisition failed, or Altinn returned a non-scope 400 that is an integration error rather than a consumer input error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"503":{"description":"Either Altinn System Register URL is not configured in the server environment, OR the idempotency reservation service is temporarily unavailable (error_code=UPSTREAM_UNAVAILABLE), OR the conflict-lock RPC is unavailable (error_code=SAFETY_SYSTEM_UNAVAILABLE — Amendment 61 §5.1, fail-CLOSED). Clients using Idempotency-Key should retry with the same key. **Branch-specific response header (NOT advertised at the union level because it is variant-specific):** the `SAFETY_SYSTEM_UNAVAILABLE` branch emits `X-Apier-Skip-Idempotency-Cache: 1`; the `UPSTREAM_UNAVAILABLE` branch does NOT. See `#/components/responses/SafetyUnavailable503` for the dedicated variant shape.","headers":{"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"$ref":"#/components/headers/LinkServiceDescriptorHeader"}},"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/UpstreamUnavailableBody"},{"$ref":"#/components/schemas/SafetyUnavailable503Body"}],"discriminator":{"propertyName":"error_code","mapping":{"UPSTREAM_UNAVAILABLE":"#/components/schemas/UpstreamUnavailableBody","SAFETY_SYSTEM_UNAVAILABLE":"#/components/schemas/SafetyUnavailable503Body"}}}}}}}}},"/api/v1/company/{org}/audit":{"parameters":[{"$ref":"#/components/parameters/CorrelationIdHeader"}],"get":{"summary":"Read consumer's own audit trail for a company","description":"Returns the slice of the immutable Sporingslogg (audit_log table) that THIS consumer wrote against the URL's org_number. Cross-consumer visibility is structurally impossible — the reader filters on `consumer_id = <authed-consumer>` AND `org_number = <url-org>` as a parameterized binding before any other predicate, and rows belonging to any other consumer never appear regardless of scope, role, or operator status. Useful for accounting firms reconciling activity (`regnskapsbyrå` reading every `delegation.create` / `gov_api.call` row their agent generated for a client — see the closed `action` enum below for the full vocabulary), AI-agent operators debugging filing flows (filter on `initiated_by=agent`), and dispute defense (Skatteforvaltningsforskriften and bokføringsloven both impose retention obligations on the agent of record; this endpoint provides the forensic trail). The response surfaces `correlation_id` (joins to provenance_log, evaluation_snapshots, compliance_state_events from the same request), `initiated_by` (human / agent / cron / system / unknown — classified by src/lib/audit/initiator.ts at write time), and `schema_version` (the OpenAPI contract version at write time so a future schema break is traceable). Sensitive keys in the `details` JSONB are AUTO-REDACTED to `[REDACTED]` by the read-side scrubber (deny-list pattern: any key containing token / secret / password / bearer / authorization / jwt / api_key / private_key / credential / client_secret / refresh_token, case-insensitive); the write-side redactor is the primary defence and the read-side scrubber is belt-and-suspenders for any pre-correlation-id rows. Pagination is keyset on (timestamp DESC, id DESC) with `limit + 1` lookahead — `next_before` and `next_before_id` from the previous response form the cursor for the next page; rows with identical timestamps tiebreak deterministically on id so a paged scan never duplicates or skips. Empty result for a valid (consumer, org) pair returns 200 with the full envelope `data: { data: [], pagination: { limit, has_more: false } }` (NEVER 404 — that would leak existence across consumers). Query parameters validated by Zod at route boundary: `limit` is `z.coerce.number().int().min(1).max(200).default(50)` — string→number coercion of valid numeric inputs (`?limit=50` → `50`) IS part of the contract because URL query parameters arrive as strings via `URLSearchParams`; non-numeric or out-of-range inputs (`?limit=abc`, `?limit=0`, `?limit=999`) fail validation explicitly rather than silently normalising; `before` is `z.string().datetime({ offset: true })`; `before_id` is `z.string().uuid()`; `since` and `until` are `z.string().datetime({ offset: true })` (the `{ offset: true }` modifier accepts ISO 8601 with explicit `Z` or `+HH:MM` offset — bare local times are rejected); `action` and `initiated_by` are closed enums (see schemas). Validation uses `safeParse` returning a structured 400 response with `error_code: VALIDATION_FAILED` and per-field `details[]` on any failure — no swallowed errors, no silent dropping of invalid fields. Source: `src/app/api/v1/company/[org]/audit/route.ts:QuerySchema`.","operationId":"readCompanyAuditTrail","x-required-scope":"read:audit","x-rate-limit-category":"company_data","tags":["Company"],"security":[{"BearerAuth":[]}],"parameters":[{"name":"org","in":"path","required":true,"schema":{"type":"string","pattern":"^[0-9]{9}$"},"description":"9-digit Norwegian organisation number (Brønnøysund-issued). Used as a within-namespace filter — the auth boundary is the API key's resolved `consumer_id`, not this URL parameter. Any other shape returns 400 VALIDATION_FAILED before the audit table is touched.","example":"123456789"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"description":"Page size. Defaults to 50; max 200. Internally the reader fetches limit+1 to compute has_more without an extra COUNT round-trip — the (limit+1)-th row is sliced off and only its presence is surfaced via `pagination.has_more`."},{"name":"before","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"Keyset cursor — pass `pagination.next_before` from the previous response. Combined with `before_id`; supplying one without the other returns 400 VALIDATION_FAILED (the cursor is a tuple, not two independent fields). Returns rows STRICTLY OLDER than the (timestamp, id) tuple."},{"name":"before_id","in":"query","required":false,"schema":{"type":"string","format":"uuid"},"description":"Keyset cursor companion — pass `pagination.next_before_id` from the previous response. The id tiebreak ensures rows with identical timestamps page deterministically (never duplicated, never skipped) under concurrent writes."},{"name":"since","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"Inclusive lower bound on timestamp, RFC 3339 with timezone (`Z` or `+HH:MM` both accepted). Compared as Date instants, so a mixed-offset (since=`Z`, until=`+02:00`) range is correctly ordered."},{"name":"until","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"Inclusive upper bound on timestamp, RFC 3339 with timezone. If both `since` and `until` are present, `since <= until` (compared as instants); otherwise 400 VALIDATION_FAILED with a field-level message."},{"name":"action","in":"query","required":false,"schema":{"$ref":"#/components/schemas/AuditAction"},"description":"Filter to one specific audit action. Whitelisted enum — see the shared `AuditAction` schema for the full vocabulary. Anything outside the list returns 400 VALIDATION_FAILED. Both this query parameter AND the response body's `action` field $ref the same schema component, so adding a new value happens in ONE place."},{"name":"initiated_by","in":"query","required":false,"schema":{"$ref":"#/components/schemas/AuditInitiatedBy"},"description":"Filter by initiator classification — see the shared `AuditInitiatedBy` schema for the full enum semantics. Both this query parameter AND the response body's `initiated_by` field $ref the same schema component."}],"responses":{"200":{"description":"Page of audit entries for THIS consumer × THIS org, ordered timestamp DESC, id DESC. Pagination via the `next_before` + `next_before_id` cursor pair when `has_more` is true. Empty data array when the consumer has no audit rows for the org (NEVER 404 — that would leak existence across consumers).","headers":{"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"$ref":"#/components/headers/LinkServiceDescriptorHeader"},"X-RateLimit-Limit":{"description":"Per-key minute ceiling for the consumer's tier (Category B). Free 30, Starter 150, Professional 300, Enterprise unlimited.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining requests in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix seconds when the window closes.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["success","data","_meta"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["data","pagination"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AuditLogEntry"}},"pagination":{"$ref":"#/components/schemas/AuditPagination"}}},"_meta":{"allOf":[{"$ref":"#/components/schemas/TrustMetadata"},{"type":"object","required":["response_timestamp","response_hash","schema_version"]}]}}},"example":{"success":true,"data":{"data":[{"id":"550e8400-e29b-41d4-a716-446655440000","timestamp":"2026-04-25T10:00:00.000Z","org_number":"123456789","action":"gov_api.call","gov_api_response_status":200,"rule_version":"2026-04-15-mva-v3","system_user_id":"sys-user-abc","correlation_id":"6ba7b810-9dad-41d1-80b4-00c04fd430c8","initiated_by":"agent","schema_version":"1.0.5","details":{"filing_type":"amelding","altinn_receipt":"ALT-12345"}}],"pagination":{"limit":50,"has_more":false}},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-25T12:00:00.000Z","last_verified":"2026-04-25T12:00:00.000Z","source":"apier.no","schema_version":"1.0.5","response_timestamp":"2026-04-25T12:00:00.000Z","response_hash":"sha256:0000000000000000000000000000000000000000000000000000000000000000"}}}}},"400":{"description":"Validation error — invalid org path, malformed query parameter, since > until, or before/before_id supplied without its companion (the keyset cursor is a tuple).","headers":{"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"$ref":"#/components/headers/LinkServiceDescriptorHeader"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorWithStrictProvenance"},"examples":{"invalid_org":{"summary":"org is not 9 digits","value":{"success":false,"error_code":"VALIDATION_FAILED","explanation":{"summary":"Invalid request parameters","details":[{"field":"org","message":"Must be exactly 9 digits"}]},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-25T12:00:00.000Z","last_verified":"2026-04-25T12:00:00.000Z","source":"apier.no","schema_version":"1.0.5","response_timestamp":"2026-04-25T12:00:00.000Z","response_hash":"sha256:0000000000000000000000000000000000000000000000000000000000000000"}}},"since_after_until":{"summary":"since > until — date range inverted","value":{"success":false,"error_code":"VALIDATION_FAILED","explanation":{"summary":"Invalid query parameters","details":[{"field":"since","message":"since must be earlier than or equal to until"}]},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-25T12:00:00.000Z","last_verified":"2026-04-25T12:00:00.000Z","source":"apier.no","schema_version":"1.0.5","response_timestamp":"2026-04-25T12:00:00.000Z","response_hash":"sha256:0000000000000000000000000000000000000000000000000000000000000000"}}},"partial_cursor":{"summary":"before supplied without before_id (keyset cursor is a tuple)","value":{"success":false,"error_code":"VALIDATION_FAILED","explanation":{"summary":"Invalid query parameters","details":[{"field":"before_id","message":"before and before_id must be supplied together (keyset cursor is a tuple)"}]},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-25T12:00:00.000Z","last_verified":"2026-04-25T12:00:00.000Z","source":"apier.no","schema_version":"1.0.5","response_timestamp":"2026-04-25T12:00:00.000Z","response_hash":"sha256:0000000000000000000000000000000000000000000000000000000000000000"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"API key is valid but the consumer's scopes do not include `read:audit` (or the wildcard `read:*` which would also grant it). Default keys created via POST /api/v1/admin/keys land with `[\"read:*\"]` (the createApiKey contract accepts only `consumer_id` + `label` — scope assignment is not exposed on the public admin API today) and DO grant this scope; this 403 fires only when the key was issued with a narrower scope set (e.g. `[\"read:brreg\", \"read:changes\"]`) via an out-of-band operator workflow. Remediation: ask an operator to issue a NEW key that includes `read:audit` (or `read:*`) via POST /api/v1/admin/keys, then revoke the narrower key via DELETE /api/v1/admin/keys/{id} — the public admin API does not expose scope mutation on existing keys.","headers":{"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"$ref":"#/components/headers/LinkServiceDescriptorHeader"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorWithStrictProvenance"},"examples":{"scope_insufficient":{"summary":"Key lacks read:audit scope","value":{"success":false,"error_code":"SCOPE_INSUFFICIENT","explanation":{"summary":"API key does not have the required scope","why":"API-nøkkelen mangler scope read:audit (eller wildcard-varianten read:*). Endepunktet leverer din egen sporingslogg (Sporingslogg / audit_log) for én organisasjon og krever at scopet er aktivert på nøkkelen. Standard nøkler opprettet via POST /api/v1/admin/keys får `[\"read:*\"]` (createApiKey-kontrakten i denne spec-en aksepterer bare `consumer_id` + `label` — scope-tildeling er ikke eksponert på det offentlige admin-API-et i dag) og dekker dermed read:audit; denne feilen oppstår kun når operatøren har utstedt en smalere nøkkel via en intern operatørflyt.","fix_steps":["Be en operatør (eller bruk operatør-/admin-dashbordet når det lanseres) om å utstede en NY bredere nøkkel som inkluderer read:audit (eller read:*) via operatørflyten — POST /api/v1/admin/keys eksponerer ikke scope-konfigurasjon, så ny utstedelse er den eneste veien — og tilbakekall deretter den smalere nøkkelen via DELETE /api/v1/admin/keys/{id}","Standard nøkkel-utstedelse via POST /api/v1/admin/keys gir `[\"read:*\"]` som dekker read:audit — denne nøkkelen vil aldri trigge denne 403-en"]},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-25T12:00:00.000Z","last_verified":"2026-04-25T12:00:00.000Z","source":"apier.no","schema_version":"1.0.5","response_timestamp":"2026-04-25T12:00:00.000Z","response_hash":"sha256:0000000000000000000000000000000000000000000000000000000000000000"}}}}}}},"429":{"$ref":"#/components/responses/RateLimitExceeded"},"500":{"description":"Unexpected failure reading the audit trail. Internal details never leak in the response body — only error_code AUDIT_QUERY_FAILED and a generic summary; the full failure context goes to Sentry under the request's correlation_id.","headers":{"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"$ref":"#/components/headers/LinkServiceDescriptorHeader"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorWithStrictProvenance"},"examples":{"query_failed":{"summary":"Reader threw — failure context in Sentry, not the response; envelope carries the full Compliance Explainer (summary / why / fix_steps / relevant_link / legal_basis)","value":{"success":false,"error_code":"AUDIT_QUERY_FAILED","explanation":{"summary":"Failed to read the audit trail","why":"En uventet intern feil oppstod under lesing av sporingsloggen (Sporingslogg). Feilen er logget til Sentry under denne forespørselens correlation_id; ingen interne detaljer lekker til denne responsen.","fix_steps":["Prøv på nytt om noen minutter — feilen kan være forbigående","Hvis den vedvarer: kontakt support@apier.no med X-Correlation-ID-headeren fra denne responsen"],"relevant_link":"https://docs.apier.no/errors/AUDIT_QUERY_FAILED","legal_basis":"Bokføringsloven § 13 / Skatteforvaltningsforskriften — agent-of-record retention obligations the audit trail exists to support"},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-25T12:00:00.000Z","last_verified":"2026-04-25T12:00:00.000Z","source":"apier.no","schema_version":"1.0.5","response_timestamp":"2026-04-25T12:00:00.000Z","response_hash":"sha256:0000000000000000000000000000000000000000000000000000000000000000"}}}}}}}}}},"/api/v1/company/{org}/context":{"parameters":[{"$ref":"#/components/parameters/CorrelationIdHeader"}],"get":{"summary":"Combined Tier 1 + Tier 2 company context","description":"Returns the canonical view of a Norwegian organisation — public Brønnøysund-sourced Tier 1 fields (name, entity_type, NACE codes, status, municipality, role holders) plus the gated Tier 2 commercial metrics (employee_count, annual_turnover, MVA status) when the consumer has an active delegation for the org. The endpoint is cache-aware: a 24-hour freshness window keeps hot orgs cheap (`_meta.served_from = 'cache'`); beyond that the gateway fetches from Brønnøysund and upserts the result (`_meta.served_from = 'live'`). If Brønnøysund is unreachable AND the local cache is still within a 7-day hard cap, the gateway serves the stored row with `_meta.stale = true` so callers can reason about freshness; if the cache is absent or older than 7 days the endpoint returns 503 UPSTREAM_UNAVAILABLE. Tier 2 is null for every caller who does not already hold a delegation — `data.tier_2_note` (Norwegian) then points the caller at POST /api/v1/auth/system-user/delegate as the remediation path. `data.aareg` carries the NAV Aa-registeret aggregate block (active/full-time/part-time/freelance counts + employment types) when the consumer has the `nav:aareg/v1/arbeidsforhold` scope delegated; aggregate counts only — no individual identities. Independent of the commercial Tier 2 delegation: a consumer may hold one without the other.","operationId":"getCompanyContext","x-required-scope":"read:brreg","tags":["Company"],"security":[{"BearerAuth":[]}],"parameters":[{"name":"org","in":"path","required":true,"schema":{"type":"string","pattern":"^[0-9]{9}$"},"description":"9-digit Norwegian organisation number (Brønnøysund-issued). Any other shape returns 400 VALIDATION_FAILED before the cache is touched.","example":"123456789"}],"responses":{"200":{"description":"Company context served. Callers MUST branch on `data.data_tier` to know whether Tier 2 fields are available, and on `_meta.served_from` / `_meta.stale` to reason about freshness.","content":{"application/json":{"schema":{"type":"object","required":["success","data","_meta"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"$ref":"#/components/schemas/CompanyContext"},"_meta":{"$ref":"#/components/schemas/TrustMetadata"}}},"examples":{"tier_1_cache_hit":{"summary":"Fresh cache (≤24h), consumer has no Tier 2 delegation","description":"Skatteetaten scope identifiers are pending approval at the source registry — examples use neutral placeholders until the approved values are confirmed.","value":{"success":true,"data":{"org_number":"123456789","name":"Eksempel AS","entity_type":"AS","nace_codes":["62.010"],"status":"active","municipality":"OSLO","registration_date":"2010-05-12T00:00:00.000Z","signaturrett":[{"role":"Signatur","name":"Ola Nordmann"}],"prokura":[],"data_tier":"tier_1","tier_2":null,"tier_2_note":"Kommersielle felter (ansatte, omsetning, balansesum, MVA-status) krever en aktiv systembruker-delegering for organisasjonen. Opprett en delegering via POST /api/v1/auth/system-user/delegate og be signaturrett hos organisasjonen om å godkjenne den.","upgrade_path":"For å få tilgang til ansatt-antall, omsetning og balansesum, må selskapet delegere tilgang til Apier via altinn.no/profil. Se /recipes for detaljer.","aareg":{"available":false,"reason":"DELEGATION_MISSING_NAV_AAREG"},"skatteetaten":{"mva_register":{"available":false,"reason":"DELEGATION_MISSING","scope":"<scope_mva_register_pending_approval>"},"mva_meldinger":{"available":false,"reason":"DELEGATION_MISSING","scope":"<scope_mva_meldinger_pending_approval>"},"skatteoppgjor":{"available":false,"reason":"DELEGATION_MISSING","scope":"<scope_skatteoppgjor_pending_approval>"}}},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-18T09:00:00.000Z","last_verified":"2026-04-18T09:00:00.000Z","source":"apier.no","schema_version":"1.0.5","served_from":"cache"}}},"tier_2_live_fetch":{"summary":"Stale cache — refreshed from Brønnøysund — consumer has active Tier 2 delegation","value":{"success":true,"data":{"org_number":"123456789","name":"Eksempel AS","entity_type":"AS","nace_codes":["62.010"],"status":"active","municipality":"OSLO","registration_date":"2010-05-12T00:00:00.000Z","signaturrett":[{"role":"Signatur","name":"Ola Nordmann"}],"prokura":[],"data_tier":"tier_2","tier_2":{"employee_count":42,"annual_turnover":10000000,"total_assets":5000000,"mva_registered":true,"mva_registration_date":"2020-03-01T00:00:00.000Z"},"tier_2_note":null,"upgrade_path":null,"aareg":{"available":true,"active_count":12,"full_time_count":8,"part_time_count":3,"freelance_count":1,"employment_types":["full_time","part_time","freelance"],"last_checked_at":"2026-04-18T10:00:00.000Z"},"skatteetaten":{"mva_register":{"available":true,"mva_registered":true,"registration_date":"2020-03-01T00:00:00.000Z","filing_frequency":"bimonthly","last_checked_at":"2026-04-18T10:00:00.000Z"},"mva_meldinger":{"available":true,"filings_count":6,"filings_skipped_count":0,"last_checked_at":"2026-04-18T10:00:00.000Z"},"skatteoppgjor":{"available":true,"year":2025,"last_checked_at":"2026-04-18T10:00:00.000Z"}}},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-18T10:00:00.000Z","last_verified":"2026-04-18T10:00:00.000Z","source":"apier.no","schema_version":"1.0.5","served_from":"live"}}},"stale_cache_fallback":{"summary":"Brønnøysund unreachable — stale cache served with stale=true","description":"Skatteetaten scope identifiers are pending approval at the source registry — examples use neutral placeholders until the approved values are confirmed.","value":{"success":true,"data":{"org_number":"123456789","name":"Eksempel AS","entity_type":"AS","nace_codes":["62.010"],"status":"active","municipality":"OSLO","registration_date":"2010-05-12T00:00:00.000Z","signaturrett":[{"role":"Signatur","name":"Ola Nordmann"}],"prokura":[],"data_tier":"tier_1","tier_2":null,"tier_2_note":"Kommersielle felter (ansatte, omsetning, balansesum, MVA-status) krever en aktiv systembruker-delegering for organisasjonen. Opprett en delegering via POST /api/v1/auth/system-user/delegate og be signaturrett hos organisasjonen om å godkjenne den.","upgrade_path":"For å få tilgang til ansatt-antall, omsetning og balansesum, må selskapet delegere tilgang til Apier via altinn.no/profil. Se /recipes for detaljer.","aareg":{"available":false,"reason":"DELEGATION_MISSING_NAV_AAREG"},"skatteetaten":{"mva_register":{"available":false,"reason":"DELEGATION_MISSING","scope":"<scope_mva_register_pending_approval>"},"mva_meldinger":{"available":false,"reason":"DELEGATION_MISSING","scope":"<scope_mva_meldinger_pending_approval>"},"skatteoppgjor":{"available":false,"reason":"DELEGATION_MISSING","scope":"<scope_skatteoppgjor_pending_approval>"}}},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-16T12:00:00.000Z","last_verified":"2026-04-16T12:00:00.000Z","source":"apier.no","schema_version":"1.0.5","served_from":"cache","stale":true}}}}}}},"400":{"description":"Invalid org path parameter (must be exactly 9 digits).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"invalid_org":{"summary":"org is not 9 digits","value":{"success":false,"error_code":"VALIDATION_FAILED","explanation":{"summary":"Invalid request parameters","details":[{"field":"org","message":"Must be exactly 9 digits"}]},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-18T09:00:00.000Z","last_verified":"2026-04-18T09:00:00.000Z","source":"apier.no","schema_version":"1.0.5","response_timestamp":"2026-04-25T12:00:00.000Z","response_hash":"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Organisation does not exist in Brønnøysund Enhetsregisteret. Distinct from 503 — a 404 is authoritative (the live API returned 404), not a transient failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"unknown_org":{"summary":"Org number not in the registry","value":{"success":false,"error_code":"NOT_FOUND","explanation":{"summary":"Organisation 999999999 not found in Brønnøysund Enhetsregisteret","why":"Organisasjonsnummeret finnes ikke i Brønnøysundregisteret.","fix_steps":["Kontroller at organisasjonsnummeret er et gyldig norsk 9-sifret nummer","Søk på selskapet i Brønnøysundregisteret via brreg.no for å bekrefte eksistens"]},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-21T09:00:00.000Z","last_verified":"2026-04-21T09:00:00.000Z","source":"apier.no","schema_version":"1.0.5","response_timestamp":"2026-04-25T12:00:00.000Z","response_hash":"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}}}}}}},"429":{"$ref":"#/components/responses/RateLimitExceeded"},"500":{"description":"Unexpected failure computing company context. Internal details never leak in the response body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"503":{"description":"Brønnøysund is unreachable AND no serviceable cache exists (no row, or the cached row is older than the 7-day hard cap). NOT used for unknown-org cases — those return 404.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"upstream_unavailable":{"summary":"Upstream down, no serviceable cache","value":{"success":false,"error_code":"UPSTREAM_UNAVAILABLE","explanation":{"summary":"Brønnøysund is unreachable and no serviceable cache is available","why":"Oppdaterte data for denne organisasjonen er ikke tilgjengelige akkurat nå — Brønnøysund svarer ikke, og vi har ingen hurtigbuffer nyere enn 7 dager.","fix_steps":["Prøv igjen om noen minutter","Kontakt support dersom problemet vedvarer"]},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-18T09:00:00.000Z","last_verified":"2026-04-18T09:00:00.000Z","source":"apier.no","schema_version":"1.0.5","response_timestamp":"2026-04-25T12:00:00.000Z","response_hash":"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}}}}}}}}}},"/api/v1/company/{org}/obligations":{"parameters":[{"$ref":"#/components/parameters/CorrelationIdHeader"}],"get":{"summary":"Regulatory obligations evaluated for a specific company","description":"Returns every regulatory obligation the Universal Rulebook evaluates for a Norwegian organisation, one EvaluationResult entry per rule whose `entity_types[]` matches the company's entity_type. The response carries all three verdicts side-by-side so agents can see the full picture: `applicable` (every condition matched — the obligation applies), `not_applicable` (at least one condition failed — the obligation does not apply), and `insufficient_data` (a Tier 2 field the rule reads is null or the Tier 2 block is absent — the agent should surface a delegation path rather than decide yes/no). Do not filter server-side; agents filter client-side if they want a narrower view. `data_tier` is `tier_2` iff a Tier 2 delegation is active AND the Registry has populated Tier 2 fields; `tier_1` responses will carry `insufficient_data` for every rule whose conditions reference a Tier 2 field, and `upgrade_path` carries a Norwegian call-to-action pointing at the Altinn delegation flow. When `data_tier` is already `tier_2`, `upgrade_path` is null. Only rules whose citations have been human-verified against Lovdata (migration 014 — `legal_reference_verified_at IS NOT NULL`) reach this endpoint; unverified rules stay in staging until a human confirms them.","operationId":"getCompanyObligations","x-required-scope":"read:brreg","tags":["Company"],"security":[{"BearerAuth":[]}],"parameters":[{"name":"org","in":"path","required":true,"schema":{"type":"string","pattern":"^[0-9]{9}$"},"description":"9-digit Norwegian organisation number (Brønnøysund-issued). Any other shape returns 400 VALIDATION_FAILED before the cache is touched.","example":"123456789"}],"responses":{"200":{"description":"Obligations evaluated. Ordering is deterministic — `rule_id` ASC — per the evaluator's Rule 9 contract; repeated calls over unchanged rules + company data produce identical arrays.","content":{"application/json":{"schema":{"type":"object","required":["success","data","_meta"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["org_number","entity_type","data_tier","obligations","upgrade_path"],"properties":{"org_number":{"type":"string","pattern":"^[0-9]{9}$"},"entity_type":{"type":"string","description":"Brønnøysund entity-type code (AS, ENK, ANS, DA, NUF, ...). Evaluator filters rules by matching this against each rule's `entity_types[]`."},"data_tier":{"type":"string","enum":["tier_1","tier_2"],"description":"`tier_2` iff the caller's Tier 2 delegation is active AND the Registry has populated tier_2 fields; `tier_1` otherwise. When tier_1, any rule reading a Tier 2 field surfaces as `insufficient_data` and `upgrade_path` is populated."},"obligations":{"type":"array","items":{"$ref":"#/components/schemas/EvaluationResult"},"description":"Every rule whose `entity_types[]` matches the company. Sorted by `rule_id` ASC deterministically. Agents branch on `evaluation_result` (applicable / not_applicable / insufficient_data)."},"upgrade_path":{"type":"string","nullable":true,"description":"Norwegian call-to-action pointing at the Altinn delegation flow + /recipes for the full walkthrough. Populated when `data_tier === 'tier_1'`, null when Tier 2 is already active."}}},"_meta":{"$ref":"#/components/schemas/RulebookTrustMetadata"}}},"examples":{"tier_1_AS_insufficient_data":{"summary":"AS with no Tier 2 delegation — revisor/yrkesskade rules surface as insufficient_data, upgrade_path populated","value":{"success":true,"data":{"org_number":"123456789","entity_type":"AS","data_tier":"tier_1","obligations":[{"rule_id":"AARSREGNSKAP_AS","obligation_name":"aarsregnskap_as","description":"Aksjeselskap leverer årsregnskap til Regnskapsregisteret innen 31. juli.","frequency":"annual","legal_reference":"Regnskapsloven § 8-2","data_tier_required":"tier_1","evaluation_result":"applicable","reason":"rule AARSREGNSKAP_AS applies (no conditions)","deadline_rule":"annual_july_31"},{"rule_id":"REVISOR_THRESHOLD_REVENUE_AS","obligation_name":"revisor_required","description":"Aksjeselskap med omsetning på 7 mill. kr eller mer må ha revisor.","frequency":"annual","legal_reference":"Aksjeloven § 7-6 første ledd nr. 1, jf. forskrift om terskelverdier § 1 nr. 1","data_tier_required":"tier_2","evaluation_result":"insufficient_data","reason":"Tier 2 delegation required to evaluate this rule (missing field: annual_turnover)","deadline_rule":"informational"},{"rule_id":"YRKESSKADEFORSIKRING","obligation_name":"yrkesskadeforsikring","description":"Arbeidsgiver med ansatte må ha yrkesskadeforsikring.","frequency":"continuous","legal_reference":"Yrkesskadeforsikringsloven § 3","data_tier_required":"tier_2","evaluation_result":"insufficient_data","reason":"Tier 2 delegation required to evaluate this rule (missing field: employee_count)","deadline_rule":"continuous"}],"upgrade_path":"For å få full vurdering av pliktene som krever Tier 2-data må selskapet delegere tilgang til Apier via altinn.no/profil. Se /recipes for detaljer."},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-21T09:00:00.000Z","last_verified":"2026-04-20T12:00:00.000Z","source":"apier.no","data_source":"Brønnøysund Enhetsregisteret + Apier Universal Rulebook","legal_basis":"NLOD — public registry reuse","served_from":"cache","schema_version":"1.0.5","rule_count":3,"applicable_count":1,"not_applicable_count":0,"insufficient_data_count":2}}}}}}},"400":{"description":"Invalid org path parameter (must be exactly 9 digits, no whitespace, no hyphens). The error `_meta` carries `data_source`, `legal_basis`, and `schema_version` so clients on the error path see the same Rulebook provenance as 200 responses.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RulebookApiError"}}}},"401":{"description":"Authentication failed — missing, malformed, invalid, or revoked API key. Middleware-generated but enriched at the route boundary so the envelope's `_meta` carries the Rulebook-required `data_source`, `legal_basis`, and `schema_version` (CLAUDE.md Rule 3).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RulebookApiError"}}}},"404":{"description":"Organisation does not exist in Brønnøysund Enhetsregisteret. Authoritative — the BRREG live API returned 404.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RulebookApiError"}}}},"429":{"$ref":"#/components/responses/RulebookRateLimitExceeded"},"500":{"description":"Unexpected failure while evaluating obligations (rule fetch, evaluator, or Supabase error). Internal details never leak in the response body per CLAUDE.md Rule 24; the cause is captured to Sentry server-side. A 500 here also surfaces when migration 014 has not been applied to the target environment — `getActiveRules` filters `legal_reference_verified_at IS NOT NULL`, so the missing column fails the rule query rather than degrading to an empty obligations array.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RulebookApiError"}}}},"503":{"description":"Brønnøysund is unreachable (retry budget exhausted on 5xx / network / timeout). Distinct from 404: an unknown org returns 404 NOT_FOUND, while a transient upstream failure returns 503 UPSTREAM_UNAVAILABLE. The response explanation names `Brønnøysund` as `upstream_system` so agents can branch on it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RulebookApiError"}}}}}}},"/api/v1/company/{org}/summary":{"parameters":[{"$ref":"#/components/parameters/CorrelationIdHeader"}],"get":{"summary":"Combined compliance summary — the Category B front-door endpoint","description":"The fastest way for an AI agent to orient itself against a Norwegian organisation: one HTTP round-trip returns the company's entity_type + data tier, the full obligation catalogue the Universal Rulebook evaluates for that entity_type, and the concrete rolling deadline calendar — side-by-side with `_meta` trust metadata naming the upstream source and legal basis for reuse.\n\n## What this endpoint returns (and does NOT return)\n\nThe response is deliberately narrow: `data.org_number`, `data.entity_type`, `data.data_tier`, `data.obligations`, `data.deadlines`, `data.upgrade_path`, and `_meta`. It does NOT include the full `/context` surface — no `name`, `status`, `municipality`, `nace_codes`, `signaturrett`, `prokura`, or role holders. If you need those, call `/api/v1/company/{org}/context` alongside this endpoint; both are backed by the same BRREG cache so the extra call is cheap.\n\n## When to call this\n\nAgents should make this endpoint their FIRST call when onboarding a new organisation — the fan-out saves two round-trips (/obligations + /deadlines) and guarantees both views are computed from the same rule version and company snapshot, so downstream reasoning doesn't have to reconcile drifting data.\n\n## Response shape (composition, not new logic)\n\nThe response composes outputs of three already-published pipelines — call-site semantics are identical to the individual endpoints:\n  - `data.obligations`: every rule whose `entity_types[]` matches the company, one `EvaluationResult` each, with all three verdicts (`applicable` / `not_applicable` / `insufficient_data`) side-by-side so agents can see the full picture. Same shape and ordering (`rule_id` ASC) as `/api/v1/company/{org}/obligations`.\n  - `data.deadlines`: per-period expansion of applicable obligations inside a rolling `from_date` + `horizon_months` window. Entries are sorted `due_at` ASC (nulls last); the same input twice produces byte-identical output per CLAUDE.md Rule 9. Same shape as `/api/v1/company/{org}/deadlines`.\n  - `data.upgrade_path`: Norwegian call-to-action pointing at the Altinn delegation flow. Populated on `tier_1` responses, `null` on `tier_2`. Wording is entity-agnostic — it doesn't name specific obligations because the set of Tier-2-gated rules varies by entity_type.\n  - `_meta`: same `RulebookTrustMetadata` shape as `/obligations` — `rulebook_version`, `data_freshness`, `last_verified` (MIN of `legal_reference_verified_at` across the rules actually evaluated — never newer than the weakest citation), `legal_basis`, `data_source`, `schema_version`, and the four evaluation counts. Present on BOTH success and error responses so `data_source` / `legal_basis` / `schema_version` are never stripped on the 4xx/5xx path.\n\n## Tier 1 vs Tier 2 semantics\n\nWhen a Tier 2 Altinn delegation is active AND the Registry has populated Tier 2 fields for the org, `data_tier` flips to `tier_2`, `upgrade_path` is nulled, and every `deadlines[].filing_status` is resolved against the `compliance_states` surface: `filed` / `overdue` land verbatim from the compliance record; `pending` / `in_progress` / `failed` / missing rows collapse to `unknown` because the lookup doesn't see `due_at` and can't commit to `upcoming` vs `overdue` on its own (CLAUDE.md Rule 26, no silent fallbacks).\n\nOn `tier_1` the filing-status lookup is not injected, so the deadline engine auto-derives `upcoming` / `overdue` from `due_at` vs `from_date` — the honest pre-delegation view.\n\n## Rulebook verification gate\n\nOnly rules whose citations have been human-verified against Lovdata (migration 014 — `legal_reference_verified_at IS NOT NULL`) reach this endpoint. Unverified rules stay in staging until a human confirms them, preserving the CLAUDE.md Rule 8 framing (\"Verified against [source] version [X] as of [date]\").\n\n## Rate limit\n\nCategory B quotas apply (CLAUDE.md Rule 27): Free 30/min, Starter 150/min, Professional 300/min, Enterprise unlimited — stricter than Category A because the response carries personal-adjacent company data. Progressive backoff kicks in at >100 unique org numbers per hour per key.","operationId":"getCompanySummary","x-required-scope":"read:brreg","tags":["Company"],"security":[{"BearerAuth":[]}],"parameters":[{"name":"org","in":"path","required":true,"schema":{"type":"string","pattern":"^[0-9]{9}$"},"description":"9-digit Norwegian organisation number (Brønnøysund-issued). Any other shape returns 400 VALIDATION_FAILED before the cache is touched.","example":"123456789"},{"name":"from_date","in":"query","required":false,"schema":{"type":"string"},"description":"Lower bound of the deadline horizon. Accepts either a full ISO 8601 timestamp or a bare `YYYY-MM-DD` (the deadline engine snaps both to the first Oslo calendar day of their month). Defaults to today in Europe/Oslo when omitted.","example":"2026-01-01"},{"name":"horizon_months","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":60},"description":"Number of calendar months to expand forward from `from_date`. Clamped to [1, 60] — a 0-month window is always empty, and anything beyond 5 years stops being actionable because the underlying rule thresholds revise more often than that. Defaults to 12.","example":12}],"responses":{"200":{"description":"Combined summary. `data.obligations` is ordered `rule_id` ASC; `data.deadlines` is ordered `due_at` ASC (nulls last). Same input twice → byte-identical output per CLAUDE.md Rule 9.","content":{"application/json":{"schema":{"type":"object","required":["success","data","_meta"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["org_number","entity_type","data_tier","obligations","deadlines","upgrade_path"],"properties":{"org_number":{"type":"string","pattern":"^[0-9]{9}$"},"entity_type":{"type":"string","description":"Brønnøysund entity-type code (AS, ENK, ANS, DA, NUF, ...)."},"data_tier":{"type":"string","enum":["tier_1","tier_2"],"description":"`tier_2` iff an Altinn delegation is active AND the Registry has populated Tier 2 fields. On `tier_1`, any rule reading a Tier 2 field surfaces as `insufficient_data`, `upgrade_path` is populated, and deadlines fall back to auto-derived `upcoming`/`overdue`."},"obligations":{"type":"array","items":{"$ref":"#/components/schemas/EvaluationResult"},"description":"Every rule whose `entity_types[]` matches the company. Sorted `rule_id` ASC deterministically. Identical shape to `/api/v1/company/{org}/obligations`."},"deadlines":{"type":"array","items":{"$ref":"#/components/schemas/DeadlineEntry"},"description":"Per-period expansion of applicable obligations within the `from_date` + `horizon_months` window. Identical shape to `/api/v1/company/{org}/deadlines`. `filing_status` resolves against `compliance_states` on `tier_2` (definitive `filed`/`overdue` surface verbatim; everything else → `unknown`). On `tier_1`, auto-derived from `due_at` vs `from_date` (`upcoming`/`overdue`)."},"upgrade_path":{"type":"string","nullable":true,"description":"Norwegian call-to-action pointing at the Altinn delegation flow + /recipes for the full walkthrough. Populated when `data_tier === 'tier_1'`, null when Tier 2 is active. Entity-agnostic wording."}}},"_meta":{"$ref":"#/components/schemas/RulebookTrustMetadata"}}},"examples":{"tier_1_AS_insufficient_data":{"summary":"AS with no Tier 2 delegation — revisor/yrkesskade surface as insufficient_data, upgrade_path populated","value":{"success":true,"data":{"org_number":"123456789","entity_type":"AS","data_tier":"tier_1","obligations":[{"rule_id":"AARSREGNSKAP_AS","obligation_name":"aarsregnskap_as","description":"Aksjeselskap leverer årsregnskap til Regnskapsregisteret innen 31. juli.","frequency":"annual","legal_reference":"Regnskapsloven § 8-2","data_tier_required":"tier_1","evaluation_result":"applicable","reason":"rule AARSREGNSKAP_AS applies (no conditions)","deadline_rule":"annual_july_31"},{"rule_id":"REVISOR_THRESHOLD_REVENUE_AS","obligation_name":"revisor_required","description":"Aksjeselskap med omsetning på 7 mill. kr eller mer må ha revisor.","frequency":"annual","legal_reference":"Aksjeloven § 7-6 første ledd nr. 1, jf. forskrift om terskelverdier § 1 nr. 1","data_tier_required":"tier_2","evaluation_result":"insufficient_data","reason":"Tier 2 delegation required to evaluate this rule (missing field: annual_turnover)","deadline_rule":"informational"}],"deadlines":[{"obligation_id":"AARSREGNSKAP_AS","obligation_name":"aarsregnskap_as","period_label":"2026","due_at":"2026-07-31T23:59:59+02:00","submission_window_opens":"2026-01-01T00:00:00+01:00","submission_window_closes":"2026-07-31T23:59:59+02:00","adjusted_for":"none","original_due_at":"2026-07-31T23:59:59+02:00","filing_status":"upcoming","data_tier_required":"tier_1"}],"upgrade_path":"For å få full vurdering av pliktene som krever Tier 2-data må selskapet delegere tilgang til Apier via altinn.no/profil. Se /recipes for detaljer."},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-21T09:00:00.000Z","last_verified":"2026-04-20T12:00:00.000Z","source":"apier.no","data_source":"Brønnøysund Enhetsregisteret + Apier Universal Rulebook","legal_basis":"NLOD — public registry reuse","served_from":"cache","schema_version":"1.0.5","rule_count":2,"applicable_count":1,"not_applicable_count":0,"insufficient_data_count":1}}},"tier_2_AS_with_filing_status":{"summary":"AS with Tier 2 delegation — filing_status resolved from compliance_states, upgrade_path null","value":{"success":true,"data":{"org_number":"123456789","entity_type":"AS","data_tier":"tier_2","obligations":[{"rule_id":"YRKESSKADEFORSIKRING","obligation_name":"yrkesskadeforsikring","description":"Arbeidsgiver med ansatte må ha yrkesskadeforsikring.","frequency":"continuous","legal_reference":"Yrkesskadeforsikringsloven § 3","data_tier_required":"tier_2","evaluation_result":"applicable","reason":"rule YRKESSKADEFORSIKRING applies (employee_count>0 satisfied)","deadline_rule":"continuous"}],"deadlines":[{"obligation_id":"YRKESSKADEFORSIKRING","obligation_name":"yrkesskadeforsikring","period_label":"ongoing","due_at":null,"submission_window_opens":null,"submission_window_closes":null,"adjusted_for":"none","original_due_at":null,"filing_status":"filed","data_tier_required":"tier_2"}],"upgrade_path":null},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-21T09:00:00.000Z","last_verified":"2026-04-20T12:00:00.000Z","source":"apier.no","data_source":"Brønnøysund Enhetsregisteret + Apier Universal Rulebook","legal_basis":"NLOD — public registry reuse","served_from":"cache","schema_version":"1.0.5","rule_count":1,"applicable_count":1,"not_applicable_count":0,"insufficient_data_count":0}}}}}}},"400":{"description":"Invalid path or query parameter. `org` must be exactly 9 digits; `from_date` must be ISO 8601 (bare YYYY-MM-DD is accepted); `horizon_months` must be an integer in [1, 60]. The response `_meta` carries `data_source`, `legal_basis`, and `schema_version` so error-path clients see the same Rulebook provenance as the 200 path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RulebookApiError"}}}},"401":{"description":"Authentication failed — missing, malformed, invalid, or revoked API key. Middleware-generated but enriched at the route boundary so `_meta.data_source` / `legal_basis` / `schema_version` stay present on this Rulebook-influenced endpoint (CLAUDE.md Rule 3).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RulebookApiError"}}}},"404":{"description":"Organisation does not exist in Brønnøysund Enhetsregisteret. Authoritative — the BRREG live API returned 404. Distinct from 503: a transient upstream failure falls through to `UPSTREAM_UNAVAILABLE` so agents can branch on retry vs. hard-fail.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RulebookApiError"}}}},"429":{"$ref":"#/components/responses/RulebookRateLimitExceeded"},"500":{"description":"Unexpected failure evaluating the summary (context read, rule fetch, evaluator, deadline engine, or compliance-state lookup). Internal details never leak in the response body per CLAUDE.md Rule 24; the cause is captured to Sentry server-side. A 500 here also surfaces if migration 014 is not applied (the Category B gate requires `legal_reference_verified_at` on the `rules` table).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RulebookApiError"}}}},"503":{"description":"Brønnøysund is unreachable (retry budget exhausted on 5xx / network / timeout) AND no serviceable cache exists (no row, or the cached row is older than the 7-day hard cap). Distinct from 404: an unknown org returns `NOT_FOUND`. The response explanation names `Brønnøysund` as `upstream_system` so agents can branch on it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RulebookApiError"}}}}}}},"/api/v1/company/{org}/deadlines":{"parameters":[{"$ref":"#/components/parameters/CorrelationIdHeader"}],"get":{"summary":"Rolling deadline calendar for a specific company","description":"Returns the concrete per-period deadline calendar for a Norwegian organisation, keyed on the set of regulatory obligations that apply given the company's entity_type plus — when a Tier 2 delegation is in place — its commercial metrics. Thin plumbing on top of three pure libs: the Universal Rulebook evaluator produces EvaluationResult[] per company context, the Deadline Engine expands applicable obligations into concrete per-period DeadlineEntry rows with Europe/Oslo offsets and weekend + Norwegian-holiday adjustment. The horizon is a rolling window anchored on `from_date` (defaults to today in Europe/Oslo) and extending `horizon_months` into the future (default 12, valid range 1–60); entries are emitted for every period whose start falls inside the window, so MVA termin 6 (due February of the next calendar year) can legitimately appear on a request whose window ends in January. Tier 2 gating matters: when the caller has no active delegation, obligations whose applicability depends on a Tier 2 field (employee_count, annual_turnover, total_assets, mva_registered) short-circuit to `insufficient_data` at the evaluator and emit NO deadline entries — callers remediate via POST /api/v1/auth/system-user/delegate and then re-call this endpoint. `filing_status` on each entry is either `upcoming` / `overdue` derived from `due_at` vs `from_date`, `filed` when the compliance-state engine has observed a successful submission for that period, or `unknown` when no compliance-state lookup is available. Entries are sorted by `due_at` ASC (nulls last) per the Deadline Engine's determinism guarantee; the same (org_number, from_date, horizon_months) triple always produces the same array.","operationId":"getCompanyDeadlines","x-required-scope":"read:brreg","tags":["Company"],"security":[{"BearerAuth":[]}],"parameters":[{"name":"org","in":"path","required":true,"schema":{"type":"string","pattern":"^[0-9]{9}$"},"description":"9-digit Norwegian organisation number (Brønnøysund-issued). Any other shape returns 400 VALIDATION_FAILED before the cache is touched.","example":"123456789"},{"name":"from_date","in":"query","required":false,"schema":{"type":"string"},"description":"Lower bound of the horizon — either a bare `YYYY-MM-DD` date or a full ISO 8601 timestamp with offset. Defaults to today in Europe/Oslo. The engine snaps this value to first-of-month in Oslo before expansion, so an obligation whose period starts earlier in the same month is still emitted. Invalid values return 400 VALIDATION_FAILED.","example":"2026-01-01"},{"name":"horizon_months","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":60},"description":"How many calendar months forward from `from_date` to expand. Default 12; valid range 1–60. A value outside the range returns 400 VALIDATION_FAILED. Five years is the hard cap — beyond that the underlying legal thresholds (Aksjeloven, MVA-forskriften) revise more often than the horizon is useful.","example":12}],"responses":{"200":{"description":"Deadline calendar computed for the org. Deadlines are sorted by `due_at` ASC (nulls last) and deterministic across repeated calls over unchanged inputs.","content":{"application/json":{"schema":{"type":"object","required":["success","data","_meta"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["org_number","entity_type","data_tier","deadlines"],"properties":{"org_number":{"type":"string","pattern":"^[0-9]{9}$"},"entity_type":{"type":"string","description":"Brønnøysund entity-type code (AS, ENK, ANS, DA, NUF, ...). Evaluator filters rules by matching this against each rule's `entity_types[]`."},"data_tier":{"type":"string","enum":["tier_1","tier_2"],"description":"`tier_2` iff the caller's Tier 2 delegation was active AND the registry has populated tier_2 fields; `tier_1` otherwise. When tier_1, any obligation that depends on a Tier 2 field produces no deadline entries — the evaluator short-circuits to `insufficient_data`."},"deadlines":{"type":"array","items":{"$ref":"#/components/schemas/DeadlineEntry"},"description":"Per-period deadlines sorted by `due_at` ASC (nulls last), then `obligation_id` ASC."}}},"_meta":{"$ref":"#/components/schemas/TrustMetadata"}}}}}},"400":{"description":"Invalid org path parameter or query parameter (from_date not ISO 8601 / horizon_months outside 1–60).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"invalid_org":{"summary":"org is not 9 digits","value":{"success":false,"error_code":"VALIDATION_FAILED","explanation":{"summary":"Invalid request parameters","details":[{"field":"org","message":"Must be exactly 9 digits"}]},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-21T09:00:00.000Z","last_verified":"2026-04-21T09:00:00.000Z","source":"apier.no","schema_version":"1.0.5","response_timestamp":"2026-04-25T12:00:00.000Z","response_hash":"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}}},"horizon_out_of_range":{"summary":"horizon_months outside 1–60","value":{"success":false,"error_code":"VALIDATION_FAILED","explanation":{"summary":"Invalid query parameters","details":[{"field":"horizon_months","message":"Too small: expected number to be >=1"}]},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-21T09:00:00.000Z","last_verified":"2026-04-21T09:00:00.000Z","source":"apier.no","schema_version":"1.0.5","response_timestamp":"2026-04-25T12:00:00.000Z","response_hash":"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Organisation does not exist in Brønnøysund Enhetsregisteret. Authoritative — the BRREG live API returned 404.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"$ref":"#/components/responses/RateLimitExceeded"},"503":{"description":"Brønnøysund is unreachable (retry budget exhausted on 5xx / network / timeout). Distinct from 404: an unknown org returns 404 NOT_FOUND, while a transient upstream failure returns 503 UPSTREAM_UNAVAILABLE.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/altinn/list-acting-capacity":{"parameters":[{"$ref":"#/components/parameters/CorrelationIdHeader"}],"post":{"summary":"Resolve actor capacity for a person on behalf of an organisation (PR-MCP-02)","description":"Given an 11-digit Norwegian fødselsnummer or D-nummer (`fnr`) and a 9-digit organisasjonsnummer (`org_number`), returns every Altinn role the actor currently holds for that organisation plus the derived action tokens those roles legally permit. The lookup hashes the raw fnr with HMAC-SHA-256(fnr, RECEIPT_HMAC_SECRET) at the boundary; the raw value NEVER lands in the response, the audit log, the cache row, or any persisted column — only the 64-hex `fnr_hmac` echoes back. Roles are normalised through `src/lib/altinn/role-action-map.ts` (DAGL Daglig leder, LEDE Styreleder, MEDL Styremedlem, NESTL Nestleder, INNH Innehaver, REGN Regnskapsfører, REVI Revisor at v1 — every entry's role-code is verified against the Altinn role catalogue, and every per-action `legal_reference` carries a verified lovdata.no citation as of PR-LH-LOVDATA-VERIFY (2026-05-16) — DAGL/LEDE/MEDL/NESTL → aksjeloven, INNH → skatteforvaltningsloven, REGN → regnskapsførerloven, REVI → revisorloven; the `derived_actions[*].verification_pending` boolean stays on the contract for forward-compat with any future un-verified seed (post-2026-05-16 it is `false` for every production row)). The fail-closed behavior for unverified `legal_reference` placeholders IS wired (the resolver's `failClosedOnUnverified` branch) but currently unreachable: `ALTINN_MODE=live` throws `Altinn live mode not yet wired (PR-MCP-02b)` so the live path never reaches the resolver at v1. When PR-MCP-02b ships the live Altinn integration, the path activates: roles still appear in `roles[]` so the agent sees what Altinn reported, but no unverified derived_action propagates to live consumers. Results are cached in `resolved_permissions` with a hard 1-hour TTL ceiling enforced by a DB CHECK constraint; `data.cached === true` indicates a cache hit and the response was served without re-hitting Altinn. Required scope: `read:altinn`. The MCP tool `list_acting_capacity` proxies through this endpoint via the PR-070 registry pattern — REST and MCP callers share one source of truth.","operationId":"listActingCapacity","x-required-scope":"read:altinn","tags":["Altinn"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["fnr","org_number"],"properties":{"fnr":{"type":"string","pattern":"^[0-9]{11}$","description":"11-digit Norwegian fødselsnummer or D-nummer. HMAC-SHA-256 hashed at the boundary; never persisted in plaintext."},"org_number":{"type":"string","pattern":"^[0-9]{9}$","description":"9-digit Norwegian organisasjonsnummer the actor wishes to represent."}}}}}},"responses":{"200":{"description":"Actor capacity resolved. The response wraps the flat data shape under `data` (the standard /api/v1/* `{ success, data, _meta }` shape; the MCP executor builds the agent envelope from this verbatim, so returning a pre-wrapped envelope here would double-wrap on the MCP path — see round-5 fix).","headers":{"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"description":"Always `</openapi.json>; rel=\"service-desc\"` per CLAUDE.md Rule 4.","schema":{"type":"string"}},"X-RateLimit-Limit":{"description":"Per-key minute ceiling for the consumer's tier (Category B). Authenticated /api/v1/* responses expose the pacing contract on success too, not only on 429.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining requests in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix seconds when the window closes.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["success","data","_meta"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","description":"Flat data shape (round-5 fix, Cursor BugBot HIGH). The MCP executor extracts this verbatim and wraps it in the agent envelope on its side; returning a pre-wrapped `{ result, justification, metadata }` here would double-wrap on the MCP path.","required":["actor","on_behalf_of","roles","derived_actions","source_snapshot_id","role_count","fetched_at","cached","correlation_id"],"properties":{"actor":{"type":"object","required":["fnr_hmac"],"properties":{"fnr_hmac":{"type":"string","pattern":"^[a-f0-9]{64}$","description":"64-char lowercase hex HMAC-SHA-256 of the input fnr. PSEUDONYMOUS — NOT anonymous: the hash is stable per (raw fnr, server pepper) pair, so two requests for the same actor produce the same `fnr_hmac` and a third party who later learns the pepper could re-identify. Treat as personal-data-adjacent: store only when necessary (audit chains, forensic joins), never log to a third-party telemetry service, prefer correlation_id for client-side tracing."}}},"on_behalf_of":{"type":"object","required":["org_number","name"],"properties":{"org_number":{"type":"string","pattern":"^[0-9]{9}$"},"name":{"type":["string","null"]}}},"roles":{"type":"array","items":{"type":"object","required":["code","label_nb","valid_from","valid_to"],"properties":{"code":{"type":"string","pattern":"^[A-Z_]{2,16}$"},"label_nb":{"type":"string"},"valid_from":{"type":"string","format":"date-time"},"valid_to":{"type":["string","null"],"format":"date-time"}}}},"derived_actions":{"type":"array","items":{"type":"object","required":["action","via_role_code","legal_reference","verification_pending"],"properties":{"action":{"type":"string"},"via_role_code":{"type":"string"},"legal_reference":{"type":"string","description":"Lovdata.no citation. Every production row carries a verified citation as of PR-LH-LOVDATA-VERIFY (2026-05-16). The TBD-VERIFY-LOVDATA literal is retained as a runtime sentinel in src/lib/altinn/resolve-permissions.ts for any future un-verified seed (defense-in-depth) — when `verification_pending` is true the value is the sentinel literal, but no production row currently triggers this."},"verification_pending":{"type":"boolean","description":"True when `legal_reference` is the TBD placeholder. Agents should treat the legal citation as provisional in that case."}}}},"source_snapshot_id":{"type":"string","description":"Opaque upstream snapshot identifier — propagated into actor_delegations.source_snapshot_id."},"role_count":{"type":"integer","minimum":0},"fetched_at":{"type":"string","format":"date-time"},"cached":{"type":"boolean","description":"True when the answer came from resolved_permissions cache; false for cold misses."},"correlation_id":{"type":"string","format":"uuid","description":"Echoes the request's correlation_id; same value appears on the X-Correlation-ID response header."}}},"_meta":{"$ref":"#/components/schemas/TrustMetadata"}}}}}},"400":{"description":"Validation failed — fnr must be 11 digits, org_number must be 9 digits, no extra fields permitted.","headers":{"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"description":"Always `</openapi.json>; rel=\"service-desc\"` per CLAUDE.md Rule 4.","schema":{"type":"string"}},"X-RateLimit-Limit":{"description":"Per-key minute ceiling for the consumer's tier (Category B). Echoed on authenticated 4xx so retry logic has the same observability as 2xx/429 paths.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining requests in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix seconds when the window closes.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Authentication failed.","headers":{"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"description":"Always `</openapi.json>; rel=\"service-desc\"` per CLAUDE.md Rule 4.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"Scope insufficient — `read:altinn` is required.","headers":{"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"description":"Always `</openapi.json>; rel=\"service-desc\"` per CLAUDE.md Rule 4.","schema":{"type":"string"}},"X-RateLimit-Limit":{"description":"Per-key minute ceiling for the consumer's tier (Category B).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining requests in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix seconds when the window closes.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"$ref":"#/components/responses/RateLimitExceeded"},"502":{"description":"Altinn upstream unavailable. Retry shortly.","headers":{"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"description":"Always `</openapi.json>; rel=\"service-desc\"` per CLAUDE.md Rule 4.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"503":{"description":"Server misconfigured — `RECEIPT_HMAC_SECRET` missing or shorter than 64 chars. Operator action required: regenerate via `openssl rand -hex 64` and set in the deployment env.","headers":{"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"description":"Always `</openapi.json>; rel=\"service-desc\"` per CLAUDE.md Rule 4.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/auth/approval-token":{"parameters":[{"$ref":"#/components/parameters/CorrelationIdHeader"}],"post":{"summary":"Mint a single-use approval token for a medium- or high-risk action","description":"Call this endpoint AFTER a human operator in the consumer's UI explicitly confirms the action. The returned `approval_token` must be presented in the `X-Approval-Token` header on the subsequent write call and is immediately consumed there — tokens are single-use, bound to a specific `(consumer, org_number, action_id)` triple, and expire after 5 minutes (default). Low-risk read actions do NOT require an approval token; requesting one for a low-risk action returns 400 VALIDATION_FAILED so misconfigured clients find out immediately instead of silently accumulating unnecessary tokens. Plaintext token values are returned exactly once — the server stores only the SHA-256 hash. Downstream write endpoints that require approval return 403 with `AUTH_APPROVAL_REQUIRED`, `AUTH_APPROVAL_INVALID`, or `AUTH_APPROVAL_MISMATCH` (see the shared ApprovalForbidden response component). This operation is recorded in the append-only audit log (the plaintext token itself is NEVER written to audit — only the `action_id` and `expires_at`).","operationId":"mintApprovalToken","x-required-scope":"read:altinn","tags":["Auth Gateway"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["org_number","action_id"],"properties":{"org_number":{"type":"string","pattern":"^[0-9]{9}$","description":"Norwegian organisation number the action will be executed against. Exactly 9 digits.","example":"123456789"},"action_id":{"type":"string","enum":["company.read","company.obligations","company.deadlines","permissions.read","delegation.create","filing.submit","filing.modify","delegation.revoke"],"description":"Known action identifier. Only medium- and high-risk actions actually mint a token; low-risk values are accepted at the boundary so the server can return the explicit 'does not require an approval token' 400 explainer instead of a generic enum rejection — that path is documented below.","example":"filing.submit"},"expires_in_seconds":{"type":"integer","minimum":1,"maximum":3600,"description":"Override the 5-minute (300s) default token lifetime. Capped at 1 hour. Shorter windows are strongly preferred so a leaked token is only briefly useful.","example":300}}}}}},"responses":{"201":{"description":"Token minted. The plaintext `approval_token` is returned exactly once — after this, only the SHA-256 hash exists server-side.","content":{"application/json":{"schema":{"type":"object","required":["success","data","_meta"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["approval_token","expires_at","action_id","org_number","expires_in_seconds"],"properties":{"approval_token":{"type":"string","description":"Opaque, URL-safe, single-use token. Include verbatim in the `X-Approval-Token` header on the write request."},"expires_at":{"type":"string","format":"date-time","description":"ISO 8601 UTC timestamp after which the token is no longer accepted."},"action_id":{"type":"string","description":"Echoed action identifier the token authorises."},"org_number":{"type":"string","description":"Echoed 9-digit organisation number the token is bound to."},"expires_in_seconds":{"type":"integer","description":"Echoed TTL that was actually applied (defaults to 300)."}}},"_meta":{"$ref":"#/components/schemas/TrustMetadata"}}},"examples":{"minted_token":{"summary":"High-risk token minted successfully","value":{"success":true,"data":{"approval_token":"R7kP3oN5qW8eZy2_aXvBcM6jL1hG4nT0sFuEiQrD9Yk","expires_at":"2026-04-17T10:05:00.000Z","action_id":"filing.submit","org_number":"123456789","expires_in_seconds":300},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-17T10:00:00.000Z","last_verified":"2026-04-17T10:00:00.000Z","source":"apier.no","schema_version":"1.0.5","response_timestamp":"2026-04-25T12:00:00.000Z","response_hash":"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}}}}}}},"400":{"description":"Validation failed — bad org_number, unknown action_id, or a low-risk action that does not require approval.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"low_risk_action":{"summary":"Low-risk actions do not accept approval tokens","value":{"success":false,"error_code":"VALIDATION_FAILED","explanation":{"summary":"This action does not require an approval token","why":"Low-risk handlinger (read-only) utføres kun med API-nøkkel. Å generere et approval_token for slike handlinger har ingen effekt.","fix_steps":["Kall handlingen direkte med API-nøkkelen i Authorization-headeren","Bruk POST /api/v1/auth/approval-token kun for medium- eller high-risk handlinger"],"details":[{"field":"action_id","message":"Action 'company.read' is low-risk and does not require approval"}]},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-17T10:00:00.000Z","last_verified":"2026-04-17T10:00:00.000Z","source":"apier.no","schema_version":"1.0.5","response_timestamp":"2026-04-25T12:00:00.000Z","response_hash":"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimitExceeded"},"500":{"description":"Unexpected server failure while minting the token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/auth/permissions/{org}":{"parameters":[{"$ref":"#/components/parameters/CorrelationIdHeader"}],"get":{"summary":"Check the delegation state for an organisation","description":"Read-side of the Auth Gateway. Agents call this before attempting an action to discover whether the delegation required to act on behalf of a Norwegian organisation is in place. The response always uses HTTP 200 — the authorisation verdict lives in `data.status` (`full`/`partial`/`none`), and when the verdict is not `full` the response carries Compliance Explainer `fix_steps` (Norwegian text) guiding the consumer to completion. Only transport-level problems (bad org_number, missing API key, internal failure) produce non-200 responses. Organisations in the 999xxxxxx range are reserved for synthetic tests and always return a `full` mock state so sandbox clients can exercise success paths without a real delegation.","operationId":"getPermissionState","x-required-scope":"read:altinn","tags":["Auth Gateway"],"security":[{"BearerAuth":[]}],"parameters":[{"name":"org","in":"path","required":true,"schema":{"type":"string","pattern":"^[0-9]{9}$"},"description":"Norwegian organisation number. Exactly 9 digits, matching Brønnøysund Register Centre format. Any other shape returns 400 VALIDATION_FAILED before the delegation lookup runs.","example":"123456789"}],"responses":{"200":{"description":"Permission state for the requested organisation. `data.status` carries the verdict; callers MUST branch on it rather than relying on HTTP status.","content":{"application/json":{"schema":{"type":"object","required":["success","data","_meta"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["org_number","status","active_scopes","missing_scopes","delegated_rights","required_but_ungranted_rights","system_user","last_checked"],"properties":{"org_number":{"type":"string","description":"Echoed 9-digit Norwegian organisation number the state applies to.","example":"123456789"},"status":{"type":"string","enum":["full","partial","none"],"description":"Authorisation verdict. `full` means every scope required for the consumer's tier is present and the underlying delegation is active — the consumer can act. `partial` means a delegation exists but is either incomplete or not yet approved (status=pending). `none` means no delegation has been created for this (consumer, org_number) pair."},"active_scopes":{"type":"array","items":{"type":"string"},"description":"Scopes Altinn has recorded for the current delegation. Empty when status is `none`."},"missing_scopes":{"type":"array","items":{"type":"string"},"description":"Scopes required by the consumer's tier baseline that are not present on the active delegation."},"delegated_rights":{"type":"array","description":"One entry per currently active scope, giving the scope, when it was granted, and which Altinn system user holds it. Useful for auditing and for generating UI tables of what the agent is allowed to do.","items":{"type":"object","required":["right_id","granted_at","system_user_id"],"properties":{"right_id":{"type":"string","description":"Altinn scope identifier (e.g. 'altinn:serviceowner/instances.read')."},"granted_at":{"type":"string","format":"date-time","description":"ISO 8601 UTC timestamp when the delegation row that grants this right was created."},"system_user_id":{"type":"string","description":"Altinn system user identifier that holds this right."}}}},"required_but_ungranted_rights":{"type":"array","description":"One entry per missing scope. Each carries a Norwegian `reason` string and `fix_steps` the consumer (or its agent) can follow to complete the delegation. Populated when status is `partial` or `none`.","items":{"type":"object","required":["right_id","reason","fix_steps"],"properties":{"right_id":{"type":"string","description":"Missing scope identifier."},"reason":{"type":"string","description":"Why this scope is not granted (Norwegian where applicable)."},"fix_steps":{"type":"array","items":{"type":"string"},"description":"Actionable steps — typically 'POST /api/v1/auth/system-user/delegate …' and a reminder to have the signing authority approve the URL."}}}},"system_user":{"type":"object","nullable":true,"description":"Altinn system user currently bound to this (consumer, org) pair, or null when no delegation exists.","required":["id","valid_until"],"properties":{"id":{"type":"string","description":"Altinn-issued System User identifier."},"valid_until":{"type":"string","format":"date-time","nullable":true,"description":"ISO 8601 UTC timestamp when the underlying delegation expires. Null for indefinite."}}},"last_checked":{"type":"string","format":"date-time","description":"Server-generated ISO 8601 UTC timestamp for when this state was computed. Matches `_meta.last_verified`."}}},"_meta":{"$ref":"#/components/schemas/TrustMetadata"}}},"examples":{"full_delegation":{"summary":"Every required scope is present and the delegation is active","value":{"success":true,"data":{"org_number":"123456789","status":"full","active_scopes":["altinn:serviceowner/instances.read","altinn:serviceowner/instances.write","skatteetaten:mva"],"missing_scopes":[],"delegated_rights":[{"right_id":"altinn:serviceowner/instances.read","granted_at":"2026-04-01T00:00:00.000Z","system_user_id":"altinn-su-111"},{"right_id":"altinn:serviceowner/instances.write","granted_at":"2026-04-01T00:00:00.000Z","system_user_id":"altinn-su-111"},{"right_id":"skatteetaten:mva","granted_at":"2026-04-01T00:00:00.000Z","system_user_id":"altinn-su-111"}],"required_but_ungranted_rights":[],"system_user":{"id":"altinn-su-111","valid_until":"2027-04-01T00:00:00.000Z"},"last_checked":"2026-04-17T10:00:00.000Z"},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-17T10:00:00.000Z","last_verified":"2026-04-17T10:00:00.000Z","source":"apier.no","schema_version":"1.0.5","response_timestamp":"2026-04-25T12:00:00.000Z","response_hash":"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}}},"partial_delegation":{"summary":"Delegation exists but does not cover all required scopes","value":{"success":true,"data":{"org_number":"123456789","status":"partial","active_scopes":["altinn:serviceowner/instances.read"],"missing_scopes":["altinn:serviceowner/instances.write","skatteetaten:mva"],"delegated_rights":[{"right_id":"altinn:serviceowner/instances.read","granted_at":"2026-04-10T12:00:00.000Z","system_user_id":"altinn-su-222"}],"required_but_ungranted_rights":[{"right_id":"altinn:serviceowner/instances.write","reason":"Scopet er ikke inkludert i den aktive delegasjonen for denne organisasjonen.","fix_steps":["Opprett en ny delegasjon som inkluderer scopet altinn:serviceowner/instances.write","Eller be signaturrett hos organisasjonen om å utvide den eksisterende delegasjonen"]},{"right_id":"skatteetaten:mva","reason":"Scopet er ikke inkludert i den aktive delegasjonen for denne organisasjonen.","fix_steps":["Opprett en ny delegasjon som inkluderer scopet skatteetaten:mva","Eller be signaturrett hos organisasjonen om å utvide den eksisterende delegasjonen"]}],"system_user":{"id":"altinn-su-222","valid_until":null},"last_checked":"2026-04-17T10:00:00.000Z"},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-17T10:00:00.000Z","last_verified":"2026-04-17T10:00:00.000Z","source":"apier.no","schema_version":"1.0.5","response_timestamp":"2026-04-25T12:00:00.000Z","response_hash":"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}}},"no_delegation":{"summary":"No delegation exists — the consumer must create one before any scope is available","value":{"success":true,"data":{"org_number":"123456789","status":"none","active_scopes":[],"missing_scopes":["altinn:serviceowner/instances.read","altinn:serviceowner/instances.write","skatteetaten:mva"],"delegated_rights":[],"required_but_ungranted_rights":[{"right_id":"altinn:serviceowner/instances.read","reason":"Ingen aktiv systembruker-delegering for denne organisasjonen. Systembrukeren må opprettes og godkjennes før denne rettigheten kan brukes.","fix_steps":["POST /api/v1/auth/system-user/delegate med org_number og ønskede scopes","Be signaturrett hos organisasjonen om å godkjenne delegasjonen via delegation_url"]}],"system_user":null,"last_checked":"2026-04-17T10:00:00.000Z"},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-17T10:00:00.000Z","last_verified":"2026-04-17T10:00:00.000Z","source":"apier.no","schema_version":"1.0.5","response_timestamp":"2026-04-25T12:00:00.000Z","response_hash":"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}}}}}}},"400":{"description":"Invalid org_number format (must be exactly 9 digits).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimitExceeded"},"500":{"description":"Unexpected failure computing permission state (typically a database read failure). Internal details are never leaked in the response body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/brreg/company-profile":{"parameters":[{"$ref":"#/components/parameters/CorrelationIdHeader"}],"post":{"summary":"Resolve a Norwegian organisasjonsnummer to a structured company profile (PR-MCP-04)","description":"Given a 9-digit Norwegian organisasjonsnummer, returns a structured company profile sourced from Brønnøysund Enhetsregisteret (data.brreg.no). The agent-facing response carries the company's display name, organisational form (e.g. AS — Aksjeselskap, ENK — Enkeltpersonforetak, ASA — Allmennaksjeselskap), Norwegian industry codes (NACE) with descriptions, the registered street address (forretningsadresse), any separate business address (postadresse), the Enhetsregisteret registration date, the agent-facing status enum (`active` for operating entities, `dissolved` for deleted/bankrupt/liquidating entities), the dissolution date when known (slettedato → dissolved_at), the MVA-registered flag (registrertIMvaregisteret), and a deduplicated list of role codes assigned to natural persons (DAGL, LEDE, MEDL, NESTL, INNH, etc.). Per CLAUDE.md Rule 11 the response carries role codes ONLY — never personal identifiers (fødselsdato, personnummer, home address, email, phone). Two-layer PII defense is enforced: the parser declares an explicit allowlist (LAYER 1) and runs a recursive deep-scan with fnr-shape regex (LAYER 2) before any value reaches the response, the cache row, the audit_log, the company_delta_log, or the changes archive. Cache TTL is 24 hours; if Brønnøysund is unavailable, the response may serve a stale cached row up to 7 days old with `metadata.stale=true` and `metadata.staleness_seconds` populated. Beyond the 7-day ceiling we 503 UPSTREAM_UNAVAILABLE (silently serving older data would mislead agents). The data is Tier 1 public infrastructure under NLOD (Norsk lisens for offentlige data); no delegation is required. Required scope: `read:brreg`. The MCP tool `get_company_profile` proxies through this endpoint via the PR-070 registry pattern — REST and MCP callers share one source of truth.","operationId":"getCompanyProfile","x-required-scope":"read:brreg","tags":["Brreg"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["org_number"],"properties":{"org_number":{"type":"string","pattern":"^[0-9]{9}$","description":"9-digit Norwegian organisasjonsnummer. Validated for shape via the regex AND for mod-11 checksum after Zod (failures return distinct error codes: VALIDATION_FAILED for shape, ORG_NUMBER_INVALID_CHECKSUM for mod-11)."}}},"examples":{"valid":{"value":{"org_number":"974761076"},"summary":"Oslo kommune (real org, active)"}}}}},"responses":{"200":{"description":"Company profile resolved. The response wraps the agent envelope under `data` (the standard /api/v1/* `{ success, data, _meta }` shape; the MCP executor exposes `data` as the top-level `{ result, justification, metadata }` envelope to MCP callers).","headers":{"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"description":"Always `</openapi.json>; rel=\"service-desc\"` per CLAUDE.md Rule 4.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"type":"object","required":["success","data","_meta"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","additionalProperties":false,"required":["result","justification","metadata"],"properties":{"result":{"type":"object","additionalProperties":false,"required":["org_number","name","org_form","nace_codes","registered_address","business_address","registration_date","status","dissolved_at","mva_registered","key_people"],"properties":{"org_number":{"type":"string","pattern":"^[0-9]{9}$"},"name":{"type":"string"},"org_form":{"type":"object","additionalProperties":false,"required":["code","label"],"properties":{"code":{"type":"string","description":"Brreg organisasjonsform.kode (e.g. AS, ENK, ASA)"},"label":{"type":"string","description":"Brreg organisasjonsform.beskrivelse (e.g. Aksjeselskap)"}}},"nace_codes":{"type":"array","description":"Sorted by code asc (Rule 9 determinism). Both `code` and `description` reflect Brreg's beskrivelse — descriptions are persisted in a dedicated `nace_descriptions` JSONB column (Round-5 fix) so cache-hit responses match cache-miss responses for the same org. Description may be the empty string only for codes that pre-date the nace_descriptions column (rare: rows seeded before PR-MCP-04).","items":{"type":"object","additionalProperties":false,"required":["code","description"],"properties":{"code":{"type":"string"},"description":{"type":"string"}}}},"registered_address":{"oneOf":[{"type":"null"},{"type":"object","additionalProperties":false,"required":["street","postal_code","postal_city","country"],"properties":{"street":{"type":"string","description":"Joined street-line array (e.g. 'Storgata 1, 2. etasje')."},"postal_code":{"type":"string"},"postal_city":{"type":"string"},"country":{"type":"string","description":"Brreg's `land` field, defaults to 'Norge' when not present."}}}],"description":"Brreg `forretningsadresse` — null when the company has no registered address (rare; some legacy entities)."},"business_address":{"oneOf":[{"type":"null"},{"type":"object","additionalProperties":false,"required":["street","postal_code","postal_city","country"],"properties":{"street":{"type":"string"},"postal_code":{"type":"string"},"postal_city":{"type":"string"},"country":{"type":"string"}}}],"description":"Brreg `postadresse` — null when no separate postal address is registered. Round-4 clarification (CodeRabbit Minor): null DOES NOT mean 'equals registered_address'; agents that need a single 'where to send mail' value should fall back to registered_address explicitly when this is null."},"registration_date":{"type":["string","null"],"format":"date"},"status":{"type":"string","enum":["active","dissolved"]},"dissolved_at":{"type":["string","null"],"format":"date"},"mva_registered":{"type":"boolean"},"key_people":{"type":"array","description":"Sorted by role_code asc. Each entry is role-shaped only — `person_role_only:true` is the schema-level affirmation that no personal identifier is present.","items":{"type":"object","additionalProperties":false,"required":["role_code","role_label","person_role_only"],"properties":{"role_code":{"type":"string"},"role_label":{"type":"string"},"person_role_only":{"type":"boolean","enum":[true]}}}}}},"justification":{"type":"object","additionalProperties":false,"required":["source","source_url","fetched_at","legal_basis","data_freshness"],"properties":{"source":{"type":"string","enum":["brreg"]},"source_url":{"type":"string","format":"uri"},"fetched_at":{"type":"string","format":"date-time"},"legal_basis":{"type":"string","enum":["NLOD — Norsk lisens for offentlige data"]},"data_freshness":{"type":"string","format":"date-time"}}},"metadata":{"type":"object","additionalProperties":false,"required":["correlation_id","cached","stale"],"properties":{"correlation_id":{"type":"string","format":"uuid"},"cached":{"type":"boolean"},"stale":{"type":"boolean","description":"True only on the stale-cache fallback path (Brreg upstream unavailable, cached row within 7-day ceiling)."},"staleness_seconds":{"type":"integer","description":"Present iff stale=true."}}}}},"_meta":{"$ref":"#/components/schemas/TrustMetadata"}}}}}},"400":{"description":"Validation failure. Two distinct error codes: VALIDATION_FAILED for shape (Zod regex / unknown field / wrong type) and ORG_NUMBER_INVALID_CHECKSUM for mod-11 failure. The distinct codes let agents recover differently.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"API key lacks the required `read:brreg` scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Brønnøysund returned no record for this organisasjonsnummer (404/410). Authoritative — retrying will not change the result.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"$ref":"#/components/responses/RateLimitExceeded"},"503":{"description":"Brønnøysund is unavailable AND no fresh-enough cached row (within the 7-day ceiling) exists for this organisation. The retry guidance is in the body's `explanation.summary` text rather than a `Retry-After` header — agents should retry after roughly 60 seconds (the per-org single-flight throttle window).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/public/deadlines":{"parameters":[{"$ref":"#/components/parameters/CorrelationIdHeader"}],"get":{"summary":"Norwegian business deadlines for a given calendar year","description":"Zero-auth, deterministic, cacheable public endpoint (CLAUDE.md Rule 19 — distribution infrastructure). Returns the canonical list of universal Norwegian business deadlines for the requested `year`: the six MVA terminer, the twelve A-melding monthly deadlines, skattemelding for AS and ENK, årsregnskap for AS, and the Foretaksregisteret årsbekreftelse. The calendar is DATABASE-DRIVEN: the route fans out `getActiveRulesForEntity` across all five Norwegian entity types `[AS, ENK, ANS, DA, NUF]` (ANS/NUF have no seed rules today but participate in `applies_to_entity_types` when a cross-entity rule lists them; adding seed rules for those types later requires no code change), evaluates each rule set against a universal tier_2 context, expands the applicable obligations through the deadline engine over the requested year, and dedupes the results on (obligation_id, period) so cross-entity deadlines merge into a single row with a unioned `applies_to_entity_types`. The response is deterministic — same `(year, Rulebook version)` inputs produce byte-identical output — and `_meta.rulebook_version` / `_meta.data_freshness` carry the MAX `last_verified` across the rules that produced the calendar, giving CDN / client caches a single invalidation key.\n\nTimezone semantics (CLAUDE.md Rule 7): every `deadline` and `adjusted_from` is an ISO 8601 string with an explicit `+01:00` (CET) or `+02:00` (CEST) offset derived dynamically from Europe/Oslo — never hardcoded. The offset applies to the ADJUSTED instant, not the pre-adjustment legal date, so a 31 May Sunday (+02:00 CEST) that pushes to 1 June Monday carries the +02:00 offset that applies on 1 June. `timezone` is always `\"Europe/Oslo\"` as the canonical reference.\n\nHoliday semantics: the legal deadline is computed first (e.g. 10th of the month, 31 May, 31 July). If that date falls on a Saturday, Sunday, or a Norwegian public holiday — the five fixed (1. januar, 1. mai, 17. mai, 25. desember, 26. desember) plus the seven Easter-linked movable feasts (skjærtorsdag, langfredag, 1. and 2. påskedag, Kr. himmelfartsdag, 1. and 2. pinsedag, computed via the Meeus/Butcher Gregorian algorithm) — the deadline is pushed FORWARD day-by-day until the next business day. It is never pushed backward. The pre-adjustment legal deadline is preserved in `adjusted_from`; that field is `null` when no adjustment was needed.\n\nRate limiting (CLAUDE.md Rule 27): soft per-IP cap of 1000 requests per minute. Exceeding returns 429 with the standard `RateLimitExceeded` response.","operationId":"getPublicDeadlines","tags":["Public"],"security":[],"parameters":[{"name":"year","in":"query","required":false,"description":"Calendar year to list deadlines for. Range 2020–2100 (out-of-range returns 400 VALIDATION_FAILED to guard against absurd input that would loop in the holiday calculation). Defaults to the current Europe/Oslo calendar year when omitted.","schema":{"type":"integer","minimum":2020,"maximum":2100,"example":2026}}],"responses":{"200":{"description":"Ordered list of deadlines for the requested year. The `_meta` block reports `served_from: \"live\"` (every request hits Supabase) and carries `data_freshness` / `last_verified` equal to the MAX `last_verified` across the rules that produced the calendar — consumers can use that timestamp as a single CDN invalidation key.","headers":{"Cache-Control":{"description":"Always `public, max-age=300`. The response is deterministic for a given `(year, Rulebook `last_verified`)` pair — when the Rulebook updates, `_meta.data_freshness` shifts and downstream caches can invalidate on that field.","schema":{"type":"string"}},"X-Rulebook-Version":{"description":"Version of the Rulebook under which deadlines were computed (CLAUDE.md Rule 3).","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"type":"object","required":["success","data","_meta"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["year","deadlines"],"properties":{"year":{"type":"integer","example":2026},"deadlines":{"type":"array","description":"Sorted ascending by `deadline`.","items":{"$ref":"#/components/schemas/PublicDeadlineEntry"}}}},"_meta":{"$ref":"#/components/schemas/TrustMetadata","description":"The public endpoints populate `rulebook_version` / `data_freshness` / `last_verified` from the MAX `last_verified` across the rules that produced the response, and set `served_from: \"live\"` to reflect the per-request Supabase read."}}},"example":{"success":true,"data":{"year":2026,"deadlines":[{"obligation_id":"a-melding-2026-01","obligation_name":"A-melding for desember 2025","period":"Desember 2025","deadline":"2026-01-05T23:59:59+01:00","submission_window_closes":"2026-01-05T23:59:59+01:00","timezone":"Europe/Oslo","adjusted_from":null,"legal_reference":"A-opplysningsloven § 4","applies_to_entity_types":["AS","ENK","ANS","DA","NUF"]},{"obligation_id":"mva-termin-1-2026","obligation_name":"MVA-melding, 1. termin 2026","period":"Januar–februar 2026","deadline":"2026-04-10T23:59:59+02:00","submission_window_closes":"2026-04-10T23:59:59+02:00","timezone":"Europe/Oslo","adjusted_from":null,"legal_reference":"Skatteforvaltningsloven § 8-3","applies_to_entity_types":["AS","ENK","ANS","DA","NUF"]},{"obligation_id":"skattemelding-enk-2026","obligation_name":"Skattemelding for enkeltpersonforetak, inntektsåret 2025","period":"Inntektsåret 2025","deadline":"2026-06-01T23:59:59+02:00","submission_window_closes":"2026-06-01T23:59:59+02:00","timezone":"Europe/Oslo","adjusted_from":"2026-05-31T23:59:59+02:00","legal_reference":"Skatteforvaltningsloven § 8-2","applies_to_entity_types":["ENK"]}]},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-21T00:00:00.000Z","last_verified":"2026-04-21T00:00:00.000Z","source":"apier.no","data_source":"Brønnøysund Enhetsregisteret + Apier Universal Rulebook","legal_basis":"NLOD — public registry reuse","served_from":"live","schema_version":"1.0.5"}}}}},"400":{"description":"Year parameter out of range (2020–2100) or not an integer.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"$ref":"#/components/responses/PublicRateLimitExceeded"}}}},"/api/v1/public/obligations":{"parameters":[{"$ref":"#/components/parameters/CorrelationIdHeader"}],"get":{"summary":"Generic Norwegian business obligations by entity type","description":"Zero-auth, deterministic, cacheable public endpoint (CLAUDE.md Rule 19). Returns the generic universal obligation set for a given Norwegian entity type — `AS`, `ENK`, `ANS`, `DA`, or `NUF`. This is the TEMPLATE layer: the response describes what obligations generally apply to the entity type, not whether any specific company's thresholds are currently tripped. For company-specific evaluation (e.g. \"is THIS AS over the MVA threshold?\") call the paid `/v1/company/{org}/obligations` endpoint instead.\n\nThe template is DATABASE-DRIVEN: `getActiveRulesForEntity` pulls every verified active rule for the entity type, the pure Rulebook evaluator runs them against a tier-1-only context, and a mapping layer projects each verdict onto the published `UniversalObligation` shape. Tier-2-gated rules surface as `required: \"conditionally\"` + `tier_2_required: true`. The response is deterministic — same `(entity_type, Rulebook version)` inputs produce byte-identical output — and `_meta.rulebook_version` / `_meta.data_freshness` carry the MAX `last_verified` across the rules that produced the response. Cache for up to an hour (the Rulebook changes on DB updates, and consumers can invalidate on the `rulebook_version` delta).\n\nKey field: `tier_2_required`. Agents use it to plan ahead — when `true`, evaluating the obligation for a specific company needs Tier 2 commercial data (employee count, turnover, MVA registration). That in turn requires an Altinn delegation, so `tier_2_required: true` entries should trigger the `request-delegation` workflow before the evaluated endpoint can answer.\n\nAnother key field: `required`. Tri-state (`always` | `conditionally` | `never`). The Rulebook-driven endpoint emits `always` for rules with zero conditions and `conditionally` for every rule with at least one condition (Tier 1 or Tier 2); `never` remains a reserved value in the enum but is not currently produced because the Rulebook models entity-type exclusions by omission (a rule that doesn't apply to an entity type is simply absent from that type's rule set) rather than via an explicit \"never\" outcome. `conditionally` entries always carry a Norwegian `condition` description of the trigger; `tier_2_required: true` entries do too.\n\nEntity types `ANS`, `DA`, and `NUF` currently return an empty obligations array with a `notes` entry indicating the templates are not yet published — future release.\n\nRate limiting (CLAUDE.md Rule 27): soft per-IP cap of 1000 requests per minute. Exceeding returns 429 with the standard `RateLimitExceeded` response.","operationId":"getPublicObligations","tags":["Public"],"security":[],"parameters":[{"name":"entity_type","in":"query","required":true,"description":"Norwegian entity-type code. One of: `AS` (aksjeselskap), `ENK` (enkeltpersonforetak), `ANS` (ansvarlig selskap), `DA` (delt ansvar), `NUF` (norskregistrert utenlandsk foretak).","schema":{"type":"string","enum":["AS","ENK","ANS","DA","NUF"],"example":"AS"}}],"responses":{"200":{"description":"Generic obligation template set for the requested entity type. The `_meta` block reports `served_from: \"live\"` (every request hits Supabase for the rule set) and carries `data_freshness` / `last_verified` equal to the MAX `last_verified` across the rules that produced the response — a single CDN invalidation key.","headers":{"Cache-Control":{"description":"Always `public, max-age=3600`. The Rulebook updates on DB writes, not on code deploys; clients can key cache invalidation on `_meta.data_freshness`.","schema":{"type":"string"}},"X-Rulebook-Version":{"description":"Version of the Rulebook under which the template was generated (CLAUDE.md Rule 3).","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"type":"object","required":["success","data","_meta"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["entity_type","obligations","notes"],"properties":{"entity_type":{"type":"string","enum":["AS","ENK","ANS","DA","NUF"]},"obligations":{"type":"array","items":{"$ref":"#/components/schemas/UniversalObligation"}},"notes":{"type":"array","description":"Caveats about the template set itself — only populated for entity types that don't yet have seeded Rulebook rules (ANS, DA, NUF). An empty array for AS/ENK means the rules returned normally; a zero-length obligations array for those types would indicate a backend outage, NOT \"not yet published\".","items":{"type":"string"}}}},"_meta":{"$ref":"#/components/schemas/TrustMetadata","description":"The public endpoints populate `rulebook_version` / `data_freshness` / `last_verified` from the MAX `last_verified` across the evaluated rules, and set `served_from: \"live\"` to reflect the per-request Supabase read."}}},"examples":{"AS":{"summary":"Aksjeselskap (AS) — full template set","value":{"success":true,"data":{"entity_type":"AS","obligations":[{"obligation_id":"mva-registration","obligation_name":"Registrering i Merverdiavgiftsregisteret","category":"registration","frequency":"one-time","required":"conditionally","condition":"Påkrevd når samlet omsetning overstiger 50 000 NOK i en 12-måneders periode.","tier_2_required":true,"legal_reference":"Merverdiavgiftsloven § 2-1","source_url":"https://lovdata.no/lov/2009-06-19-58/%C2%A72-1"},{"obligation_id":"skattemelding-annual","obligation_name":"Skattemelding for formues- og inntektsskatt","category":"tax","frequency":"annual","required":"always","condition":null,"tier_2_required":false,"legal_reference":"Skatteforvaltningsloven § 8-2","source_url":"https://lovdata.no/lov/2016-05-27-14/%C2%A78-2"}],"notes":[]},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-21T00:00:00.000Z","last_verified":"2026-04-21T00:00:00.000Z","source":"apier.no","data_source":"Brønnøysund Enhetsregisteret + Apier Universal Rulebook","legal_basis":"NLOD — public registry reuse","served_from":"live","schema_version":"1.0.5"}}},"ENK":{"summary":"Enkeltpersonforetak (ENK) — typical Rulebook-driven response","value":{"success":true,"data":{"entity_type":"ENK","obligations":[{"obligation_id":"skattemelding-annual","obligation_name":"Skattemelding for enkeltpersonforetak","category":"tax","frequency":"annual","required":"always","condition":null,"tier_2_required":false,"legal_reference":"Skatteforvaltningsloven § 8-2","source_url":"https://lovdata.no/lov/2016-05-27-14/%C2%A78-2"},{"obligation_id":"a-melding-monthly","obligation_name":"A-melding","category":"reporting","frequency":"monthly","required":"conditionally","condition":"Arbeidsgivere må levere A-melding månedlig.","tier_2_required":true,"legal_reference":"A-opplysningsloven § 4","source_url":"https://lovdata.no/lov/2012-06-22-43/%C2%A74"}],"notes":[]},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-21T00:00:00.000Z","last_verified":"2026-04-21T00:00:00.000Z","source":"apier.no","data_source":"Brønnøysund Enhetsregisteret + Apier Universal Rulebook","legal_basis":"NLOD — public registry reuse","served_from":"live","schema_version":"1.0.5"}}}}}}},"400":{"description":"Missing `entity_type` or value outside the supported enum.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"$ref":"#/components/responses/PublicRateLimitExceeded"}}}},"/api/v1/tools/exchange-rate":{"parameters":[{"$ref":"#/components/parameters/CorrelationIdHeader"}],"get":{"summary":"Official Norges Bank NOK exchange rate for a currency and date","description":"Zero-auth public tool endpoint. Wraps Norges Bank's open SDMX API at `data.norges-bank.no/api/data/EXR` so agents can fetch official NOK rates without registering anywhere. All rates are quoted as `base: \"NOK\"` with the foreign currency as `quote` — the `rate` value means 1 unit of the foreign currency equals `rate` NOK.\n\n**Source of truth**: the gateway never fabricates or interpolates rates. If Norges Bank hasn't published a quotation, the gateway returns the most recent available one and surfaces the discrepancy (`data.date` is the actual publication date; `data.requested_date` is what the caller asked for). Norges Bank does not publish on weekends, Norwegian public holidays, or before ~16:00 Europe/Oslo on the current day — requests that fall in those windows transparently get the most recent prior business day's rate.\n\n**Caching**: in-memory per Lambda, 1-hour TTL. Cache hits are signalled by `data.cached: true` and `_meta.served_from: \"cache\"`; cache misses are `false` / `\"live\"`. The HTTP `Cache-Control: public, max-age=3600` header also applies, so CDNs and browsers can cache identical requests.\n\n**Rate limiting** (CLAUDE.md Rule 27): soft per-IP cap of 1000 requests per minute via the shared `withPublicRateLimit` wrapper. The standard `X-RateLimit-*` headers are present on every 200 response.","operationId":"getExchangeRate","tags":["Public"],"security":[],"parameters":[{"name":"currency","in":"query","required":true,"description":"ISO 4217 three-letter currency code (case-insensitive, upper-cased in the response). Must be a currency Norges Bank publishes — EUR, USD, GBP, SEK, DKK, and others from the EXR dataset. Unknown currencies return 404 NO_RATE_AVAILABLE.","schema":{"type":"string","pattern":"^[A-Za-z]{3}$","example":"EUR"}},{"name":"date","in":"query","required":false,"description":"Oslo calendar date in YYYY-MM-DD. Defaults to today-in-Europe/Oslo when omitted. Future dates return 400 INVALID_DATE (Norges Bank does not publish the future).","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","example":"2026-04-10"}}],"responses":{"200":{"description":"Rate returned. `data.date` equals `data.requested_date` when the underlying publication date matches; when the request fell on a weekend / holiday / pre-publication window, `data.date` is the most recent prior business day.","headers":{"Cache-Control":{"description":"Always `public, max-age=3600`.","schema":{"type":"string"}},"X-RateLimit-Limit":{"description":"Per-IP minute ceiling. Present on the normal allowed path; OMITTED on the fail-open path, where `X-RateLimit-Check: skipped` is set instead.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining requests in the current window. Omitted on the fail-open path.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix seconds when the current window closes. Omitted on the fail-open path.","schema":{"type":"integer"}},"X-RateLimit-Check":{"description":"Present with the literal value `skipped` on the rare fail-open path — i.e. when the rate-limit infrastructure was briefly unavailable and the request was allowed through without quota enforcement. The three standard `X-RateLimit-*` headers are omitted on that path. Clients can treat its presence as a transient operational signal.","schema":{"type":"string","enum":["skipped"]}}},"content":{"application/json":{"schema":{"type":"object","required":["success","data","_meta"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["base","quote","rate","date","requested_date","source","cached"],"properties":{"base":{"type":"string","enum":["NOK"],"description":"Always NOK — Norges Bank publishes all EXR rates with NOK as the base."},"quote":{"type":"string","description":"Upper-cased ISO 4217 code.","example":"EUR"},"rate":{"type":"number","description":"1 unit of `quote` = `rate` NOK.","example":11.5234},"date":{"type":"string","format":"date","description":"Actual publication date of the returned quotation — may predate `requested_date` on weekends / holidays / pre-16:00 today.","example":"2026-04-10"},"requested_date":{"type":"string","format":"date","description":"The date the caller asked for (or today-in-Oslo if they didn't supply one).","example":"2026-04-11"},"source":{"type":"string","enum":["Norges Bank"],"description":"Authoritative source."},"cached":{"type":"boolean","description":"True when the rate was served from the per-Lambda in-memory cache (1-hour TTL)."}}},"_meta":{"$ref":"#/components/schemas/TrustMetadata"}}},"example":{"success":true,"data":{"base":"NOK","quote":"EUR","rate":11.5234,"date":"2026-04-10","requested_date":"2026-04-11","source":"Norges Bank","cached":false},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-10T00:00:00.000Z","last_verified":"2026-04-11T10:00:00.000Z","source":"apier.no","served_from":"live","schema_version":"1.0.5"}}}}},"400":{"description":"INVALID_CURRENCY (not a 3-letter code) or INVALID_DATE (unparseable or in the future).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"NO_RATE_AVAILABLE — Norges Bank has no rate series for this currency, or the currency series exists but no data at or before `requested_date`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"$ref":"#/components/responses/PublicRateLimitExceeded"},"502":{"description":"UPSTREAM_UNAVAILABLE — Norges Bank timed out or returned persistent 5xx after retries. Transient; retry in a few minutes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/tools/altinn-migration":{"parameters":[{"$ref":"#/components/parameters/CorrelationIdHeader"}],"get":{"summary":"Altinn 2 role → Altinn 3 access package mapping (zero auth)","description":"Returns the Altinn 3 access package(s) that replace a legacy Altinn 2 role. Use this endpoint to plan migration ahead of the June 19, 2026 Altinn 2 cutoff: existing Altinn 2 rights do not migrate automatically, so every integration must map its required roles to the equivalent Altinn 3 access packages and re-delegate via System Users. Pass ?altinn2_code=<code> to look up a single role. Omit the query param to receive the full mapping table. Every entry ships with a verified flag — false means the mapping is a best-effort reference and must be cross-checked against DigDir before production use. Days until the cutoff are computed in Europe/Oslo and surfaced on data.days_remaining. Data source: Altinn / DigDir public documentation. No authentication required. Cacheable for 1 hour (Cache-Control: public, max-age=3600). Safe for agentic discovery, AI-search crawl, and direct human use.","operationId":"getAltinnMigration","tags":["Public"],"security":[],"parameters":[{"name":"altinn2_code","in":"query","required":false,"description":"Filter to a single Altinn 2 service or role code. Alphanumeric, 1-10 characters (case-insensitive on input; upper-cased on match). When omitted the endpoint returns the full map.","schema":{"type":"string","pattern":"^[A-Za-z0-9]{1,10}$","example":"A0208"}}],"responses":{"200":{"description":"Either the full mapping list (when `altinn2_code` is omitted) or a single matching entry. The deadline fields are present on both shapes.","headers":{"Cache-Control":{"description":"Always `public, max-age=3600`.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"oneOf":[{"type":"object","required":["success","data","_meta"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["deprecation_deadline","timezone","days_remaining","deadline_passed","mappings"],"properties":{"deprecation_deadline":{"type":"string","format":"date-time","description":"19 June 2026 23:59:59 in Europe/Oslo. ISO 8601 with Oslo offset."},"timezone":{"type":"string","enum":["Europe/Oslo"]},"days_remaining":{"type":"integer","minimum":0,"description":"Whole Oslo calendar days until the cliff. Non-negative — clamps to 0 once the deadline has passed."},"deadline_passed":{"type":"boolean","description":"True once the deprecation instant has elapsed in Europe/Oslo."},"mappings":{"type":"array","items":{"$ref":"#/components/schemas/MigrationEntry"}}}},"_meta":{"$ref":"#/components/schemas/TrustMetadata"}}},{"type":"object","required":["success","data","_meta"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["deprecation_deadline","timezone","days_remaining","deadline_passed","entry"],"properties":{"deprecation_deadline":{"type":"string","format":"date-time"},"timezone":{"type":"string","enum":["Europe/Oslo"]},"days_remaining":{"type":"integer","minimum":0},"deadline_passed":{"type":"boolean"},"entry":{"$ref":"#/components/schemas/MigrationEntry"}}},"_meta":{"$ref":"#/components/schemas/TrustMetadata"}}}]}}}},"400":{"description":"INVALID_CODE — altinn2_code present but empty, non-alphanumeric, or longer than 10 characters.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"NOT_FOUND — altinn2_code was well-formed but no entry exists for it in the static map.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"$ref":"#/components/responses/PublicRateLimitExceeded"}}}},"/api/v1/capabilities":{"parameters":[{"$ref":"#/components/parameters/CorrelationIdHeader"}],"get":{"summary":"Machine-readable capability manifest (agent discovery)","description":"Zero-auth, deterministic, cacheable for 1 hour. The agent-first counterpart to this OpenAPI spec: where OpenAPI describes HTTP shapes for humans + tool-generators, `/v1/capabilities` describes WHAT the agent can accomplish at a planning level and pairs each capability with the matching `openapi_operation_id` so the agent can jump into this spec for full signature details.\n\nThe manifest is a hand-curated typed constant in `src/lib/capabilities/manifest.ts`, NOT auto-derived from route files — that would over-share (internal health checks, cron endpoints) and drift on refactors. CLAUDE.md enforces a PR checklist line: every new public or authenticated endpoint must add an entry here. Capability `id` values and endpoint paths are APPEND-ONLY forever; renaming either is a /v2 break (CLAUDE.md Rule 10). Agents may cache the response keyed by `id`.\n\nThe top-level `rulebook_version` field is currently the stub `\"pre-rulebook\"` — it will advance when the Rulebook engine lands. Clients shouldn't parse meaning out of its value until then; just surface it.\n\nCategories: `public-tool` (zero-auth discovery / utility), `company-data` (authenticated org reads), `auth` (Altinn delegation broker + approval tokens), `admin` (operator-only, behind `ADMIN_API_KEY`), `discovery` (this endpoint's siblings). `auth: \"none\"` entries need no Authorization header; `auth: \"api_key\"` entries need the Bearer consumer key. `tier_minimum` names the lowest consumer tier that may call a capability (`null` when auth is `none` or when the route is admin-only).\n\nRate-limited at 1000/min per IP via the shared public wrapper.","operationId":"getCapabilities","tags":["Discovery"],"security":[],"responses":{"200":{"description":"Full manifest.","headers":{"Cache-Control":{"description":"Always `public, max-age=3600`.","schema":{"type":"string"}},"X-RateLimit-Limit":{"description":"Per-IP minute ceiling (1000). Present on the normal allowed path; omitted on the rare fail-open path where `X-RateLimit-Check: skipped` is set instead.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining requests in the current window. Omitted on the fail-open path.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix seconds when the current window closes. Omitted on the fail-open path.","schema":{"type":"integer"}},"X-RateLimit-Check":{"description":"Present with the literal value `skipped` on the rare fail-open path — when the rate-limit infrastructure was briefly unavailable and the request was allowed through without quota enforcement.","schema":{"type":"string","enum":["skipped"]}}},"content":{"application/json":{"schema":{"type":"object","required":["success","data","_meta"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["service","version","rulebook_version","locale","capabilities","sandbox"],"properties":{"service":{"type":"string","enum":["apier.no"]},"version":{"type":"string","example":"1"},"rulebook_version":{"type":"string","description":"Stub `pre-rulebook` until the Rulebook engine lands.","example":"pre-rulebook"},"locale":{"type":"string","enum":["nb-NO"]},"capabilities":{"type":"array","items":{"$ref":"#/components/schemas/CapabilityEntry"}},"sandbox":{"$ref":"#/components/schemas/CapabilitiesSandboxBlock"}}},"_meta":{"allOf":[{"$ref":"#/components/schemas/TrustMetadata"},{"type":"object","required":["schema_version"],"description":"`schema_version` is REQUIRED on this agent-first machine interface (CLAUDE.md Rule 23). Clients may safely key caches or migration logic off its value.","properties":{"schema_version":{"type":"string","example":"1.0.0"}}}]}}}}}},"429":{"$ref":"#/components/responses/PublicRateLimitExceeded"}}}},"/llms.txt":{"get":{"summary":"Short-form llms.txt discovery file","description":"Zero-auth, served at the domain root per the [llms.txt convention](https://llmstxt.org). Plain text, UTF-8. Points at /llms-full.txt, /openapi.json, /workflows.json, and /api/v1/capabilities for richer detail. Cacheable for 1 hour (`Cache-Control: public, max-age=3600`). Rate-limited at the CDN/edge layer, not the application layer.","operationId":"getLlmsTxt","tags":["Discovery"],"security":[],"responses":{"200":{"description":"Plain-text summary pointing at the richer discovery manifests.","headers":{"Cache-Control":{"description":"Always `public, max-age=3600`.","schema":{"type":"string"}}},"content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/llms-full.txt":{"get":{"summary":"Long-form product description for LLM context windows","description":"Zero-auth long-form prose describing what Apier does, who it's for, which Norwegian government integrations ship, the currently-live capability surface (referencing capability ids from /api/v1/capabilities verbatim), and forward-looking sections that are clearly marked as such. Plain text, UTF-8. Cacheable for 1 hour.","operationId":"getLlmsFullTxt","tags":["Discovery"],"security":[],"responses":{"200":{"description":"Plain-text marketing + product description.","headers":{"Cache-Control":{"description":"Always `public, max-age=3600`.","schema":{"type":"string"}}},"content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/workflows.json":{"get":{"summary":"Canonical agent workflows manifest","description":"Zero-auth JSON manifest of common multi-step workflows an agent might want to run. Each workflow references capability ids from /api/v1/capabilities so the published contract stays internally consistent — an agent framework can chain `capability_id` → OpenAPI `operationId` → HTTP shape in two lookups. Workflow `id` values and `steps[].capability_id` values are APPEND-ONLY forever (CLAUDE.md Rule 10); new workflows may be added, existing ones gain new optional fields but never shrink or rename. `schema_version` on both the top level and `_meta` advertises the manifest schema for client caching.\n\nEach workflow carries enriched planner metadata — `intent`, `expected_outcome`, `failure_modes`, `estimated_time_ms`, and `agent_instructions` — plus per-step `expected_response` describing the body the step returns on success. The response also includes a schema.org JSON-LD `@context` and `@graph` with one `HowTo` node per workflow, so AI crawlers that speak schema.org but not Apier's bespoke shape can still plan a multi-step flow. See /recipes for the human-readable counterpart.","operationId":"getWorkflowsJson","tags":["Discovery"],"security":[],"responses":{"200":{"description":"Workflow manifest with `_meta` carrying data_freshness and workflow_count, plus a schema.org JSON-LD `@graph` of HowTo nodes.","headers":{"Cache-Control":{"description":"Always `public, max-age=3600`.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"type":"object","required":["version","schema_version","@context","@graph","workflows","_meta"],"properties":{"version":{"type":"string","example":"1"},"schema_version":{"type":"string","example":"1.1.0"},"@context":{"type":"string","example":"https://schema.org","description":"schema.org JSON-LD context URL. Always `https://schema.org` — present so crawlers can interpret the `@graph` array as schema.org `HowTo` nodes."},"@graph":{"type":"array","description":"schema.org JSON-LD projection — one `HowTo` node per workflow, derived from the same data as `workflows`. Intended for AI crawlers that consume schema.org but don't speak the Apier-native workflow shape.","items":{"type":"object","required":["@type","@id","name","description","totalTime","step"],"properties":{"@type":{"type":"string","enum":["HowTo"],"description":"schema.org node type for each projected workflow. Always `HowTo`."},"@id":{"type":"string","format":"uri","description":"Canonical stable identifier for this workflow node. Format is `<origin>/workflows.json#<workflow.id>` where `<origin>` is derived from the deployment's NEXT_PUBLIC_APP_URL (scheme + host + port)."},"name":{"type":"string","description":"English workflow title mirrored from `workflows[].name_en`."},"description":{"type":"string","description":"English workflow summary mirrored from `workflows[].description_en`."},"totalTime":{"type":"string","description":"ISO 8601 duration approximating end-to-end workflow time, derived from `estimated_time_ms` (rounded up to the nearest whole second, minimum PT1S). Agent planners use this to decide synchronous vs queued execution.","example":"PT1S"},"step":{"type":"array","description":"Ordered schema.org HowToStep nodes mirroring `workflows[].steps`.","items":{"type":"object","required":["@type","position","name","text"],"properties":{"@type":{"type":"string","enum":["HowToStep"],"description":"schema.org node type for each step. Always `HowToStep`."},"position":{"type":"integer","minimum":1,"description":"1-based step order, mirroring `workflows[].steps[].step`."},"name":{"type":"string","description":"Step purpose mirrored from `workflows[].steps[].purpose` — why this capability call exists in the workflow."},"text":{"type":"string","description":"Expected successful response summary mirrored from `workflows[].steps[].expected_response`."}}}}}}},"workflows":{"type":"array","items":{"type":"object","required":["id","name_en","name_nb","description_en","trigger_intent_en","intent","expected_outcome","failure_modes","estimated_time_ms","agent_instructions","steps","requires_auth","tier_minimum"],"properties":{"id":{"type":"string","example":"workflow.lookup-deadlines"},"name_en":{"type":"string"},"name_nb":{"type":"string"},"description_en":{"type":"string"},"trigger_intent_en":{"type":"string"},"intent":{"type":"string","description":"Natural-language description of what the workflow achieves — the goal, not the mechanics."},"expected_outcome":{"type":"string","description":"What the agent holds in its context after the workflow completes successfully."},"failure_modes":{"type":"array","description":"Known failure-mode identifiers agents should plan for. Append-only per workflow.","items":{"type":"string"}},"estimated_time_ms":{"type":"integer","minimum":1,"description":"Rough total time for all steps end-to-end, in milliseconds. Intended for agent planners deciding whether to run a workflow synchronously or queue it."},"agent_instructions":{"type":"string","description":"Step-by-step instructions an agent can paste verbatim into a system prompt or tool-use plan. Plain text with numbered steps; uses placeholders like `<ORG_NUMBER>`, `<CURRENCY>`."},"steps":{"type":"array","items":{"type":"object","required":["step","capability_id","purpose","expected_response"],"properties":{"step":{"type":"integer","minimum":1,"description":"1-based position of this step within the workflow. Strictly ascending across `steps[]`."},"capability_id":{"type":"string","description":"Matches a capability id in /api/v1/capabilities. Lets an agent chain workflow step → capability → OpenAPI `operationId` → HTTP shape in two lookups.","example":"public.deadlines"},"purpose":{"type":"string","description":"Why this capability call exists in the workflow, expressed in agent-planning terms (e.g. 'Call GET /api/v1/...'). Human-readable but short enough to paste into a tool-use plan."},"expected_response":{"type":"string","description":"One-sentence description of the response body an agent should expect when this step succeeds."}}}},"requires_auth":{"type":"boolean"},"tier_minimum":{"anyOf":[{"type":"null"},{"type":"string","enum":["free","starter","professional","enterprise"]}]}}}},"_meta":{"type":"object","required":["data_freshness","source","schema_version","workflow_count"],"properties":{"data_freshness":{"type":"string","format":"date-time"},"source":{"type":"string","example":"apier.no internal manifest"},"schema_version":{"type":"string","example":"1.1.0"},"workflow_count":{"type":"integer","minimum":0}}}}}}}}}}},"/recipes":{"get":{"summary":"Human-readable recipes page","description":"SSR HTML counterpart to /workflows.json. Renders one section per canonical workflow: intent, numbered steps, agent_instructions, failure modes, and a copy-paste cURL example. Not a JSON API — content-type is `text/html`. Intended both for developers browsing the site and for AI crawlers that prefer HTML over the JSON manifest.\n\nEvery section on the page is derived from the same manifest `/workflows.json` serves, so the two surfaces cannot drift. Cacheable for 1 hour.","operationId":"getRecipesPage","tags":["Discovery"],"security":[],"responses":{"200":{"description":"HTML page rendering one section per workflow.","headers":{"Cache-Control":{"description":"Always `public, max-age=3600` via Next.js `revalidate`.","schema":{"type":"string"}}},"content":{"text/html":{"schema":{"type":"string"}}}}}}},"/api/v1/comparison/direct-integration":{"parameters":[{"$ref":"#/components/parameters/CorrelationIdHeader"}],"get":{"summary":"Structured Apier-vs-direct-integration comparison","description":"Zero-auth, cacheable for 1 hour. Returns the head-to-head matrix agents use to decide between Apier, direct Altinn/Brønnøysund/Skatteetaten integration, and HTML scraping.\n\nThis is **structured data for agent selection, not marketing**. Nulls on `success_rate` and `p95_latency_ms` are intentional honesty markers: for the Apier approach they ship as static placeholders derived from load-test + staging (will graduate to live values when telemetry ships); for competing approaches they stay null because Apier does not instrument them on the caller's behalf. Agents can safely distinguish 'unknown' from 'zero'.\n\nThe interesting columns for agent decision-making are the feature booleans: `error_normalization` (consistent error envelope across all four Norwegian upstreams), `deadline_intelligence` (understands MVA terminer / A-melding / skattemelding rather than just proxying HTTP), and `cross_agency_orchestration` (one call can coordinate state across multiple agencies). `complexity` + `auth_setup_steps` capture upfront integration cost. These are the capabilities Digdir will not replicate — raw speed and uptime are secondary signals.\n\nRate-limited at 1000/min per IP via the shared public wrapper.","operationId":"getComparisonDirectIntegration","tags":["Discovery"],"security":[],"responses":{"200":{"description":"Full comparison matrix.","headers":{"Cache-Control":{"description":"Always `public, max-age=3600`.","schema":{"type":"string"}},"X-RateLimit-Limit":{"description":"Per-IP minute ceiling (1000). Omitted on the fail-open path.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining requests in the current window. Omitted on the fail-open path.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix seconds when the current window closes. Omitted on the fail-open path.","schema":{"type":"integer"}},"X-RateLimit-Check":{"description":"Present with the literal value `skipped` on the rare fail-open path.","schema":{"type":"string","enum":["skipped"]}}},"content":{"application/json":{"schema":{"type":"object","required":["success","data","_meta"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"$ref":"#/components/schemas/ComparisonResponse"},"_meta":{"allOf":[{"$ref":"#/components/schemas/TrustMetadata"},{"type":"object","required":["schema_version"],"properties":{"schema_version":{"type":"string","example":"1.0.0"}}}]}}}}}},"429":{"$ref":"#/components/responses/PublicRateLimitExceeded"}}}},"/api/v1/explain":{"parameters":[{"$ref":"#/components/parameters/CorrelationIdHeader"}],"post":{"summary":"Resolve a structured Apier compliance error code into a Norwegian-bokmål Explanation envelope (PR-070f)","description":"Zero-auth, rate-limited per-IP (Category A — 1000/min soft cap). Wraps the existing Compliance Explainer (`src/lib/compliance/explainer.ts`, PR-049) over HTTP so agents that received a structured error from any /v1/* endpoint can resolve it into a one-line `summary`, a 1–3 sentence `why`, an ordered list of `fix_steps`, an optional Apier / Altinn / Skatteetaten / Brønnøysund `relevant_link`, an optional Lovdata-style `legal_basis`, and an optional `handover` block (who / where / what / why) for errors a human must resolve.\n\nPure static lookup over a closed catalogue (`EXPLAINER_ERROR_CODES`, 33 codes today across auth, validation, scope, upstream, idempotency, action-execute, government, and reliability domains). Deterministic per Rule 9: same `(error_code, context)` → byte-identical Explanation. The optional `context` object carries placeholder values (`org_number`, `role`, `scope`, `field`, `upstream_system`) that the explainer interpolates into the bokmål text — missing values fall back to a Norwegian 'ukjent <noun>' rather than leaking placeholder syntax.\n\nUnknown `error_code` returns 400 VALIDATION_FAILED; the offending value is NEVER echoed in the response body (Rule 24 information-disclosure mitigation).","operationId":"explainComplianceError","tags":["Discovery"],"security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["error_code"],"additionalProperties":false,"properties":{"error_code":{"type":"string","description":"One of `EXPLAINER_ERROR_CODES`. The /v1/capabilities response lists the full set; the most frequently asked are `AUTH_INSUFFICIENT_ROLE`, `AUTH_NO_DELEGATION`, `VALIDATION_FAILED`, `SCOPE_MISSING`, `RATE_LIMIT_EXCEEDED`, `NOT_FOUND`, `UPSTREAM_UNAVAILABLE`, `IDEMPOTENCY_KEY_MISMATCH`, `IDEMPOTENCY_IN_PROGRESS`."},"context":{"type":"object","additionalProperties":false,"description":"Optional placeholder values the explainer interpolates into the bokmål text. Missing values fall back to a Norwegian 'ukjent <noun>'.","properties":{"org_number":{"type":"string","pattern":"^\\d{9}$","description":"9-digit Norwegian organisasjonsnummer."},"scope":{"type":"string","minLength":1,"maxLength":64,"description":"Maskinporten scope, e.g. `read:brreg`."},"role":{"type":"string","minLength":1,"maxLength":64,"description":"Altinn role code, e.g. `DAGL`, `LEDE`."},"field":{"type":"string","minLength":1,"maxLength":64,"description":"Field name for VALIDATION_FAILED context, e.g. `amount`."},"upstream_system":{"type":"string","minLength":1,"maxLength":64,"description":"Upstream system name for UPSTREAM_UNAVAILABLE context, e.g. `Brønnøysund`."}}}}},"examples":{"auth_insufficient_role":{"summary":"AUTH_INSUFFICIENT_ROLE — handover-bearing","value":{"error_code":"AUTH_INSUFFICIENT_ROLE","context":{"org_number":"974761076","role":"DAGL"}}},"validation_failed":{"summary":"VALIDATION_FAILED — agent-resolvable","value":{"error_code":"VALIDATION_FAILED","context":{"field":"amount"}}}}}}},"responses":{"200":{"description":"Resolved Explanation envelope.","headers":{"X-RateLimit-Limit":{"description":"Per-IP minute ceiling (1000).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining requests in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix seconds when the current window closes.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["success","data","_meta"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["explanation"],"properties":{"explanation":{"type":"object","required":["error_code","summary","why","fix_steps","relevant_link","legal_basis","handover"],"properties":{"error_code":{"type":"string"},"summary":{"type":"string"},"why":{"type":"string"},"fix_steps":{"type":"array","items":{"type":"string"}},"relevant_link":{"type":["string","null"]},"legal_basis":{"type":["string","null"]},"handover":{"type":["object","null"],"properties":{"who":{"type":"string"},"where":{"type":"string"},"what":{"type":"string"},"why":{"type":"string"}}}}}}},"_meta":{"$ref":"#/components/schemas/TrustMetadata"}}}}}},"400":{"description":"Validation failure — unknown `error_code`, malformed body, or extra context keys. The offending `error_code` value is NEVER echoed (Rule 24).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"$ref":"#/components/responses/PublicRateLimitExceeded"}}}},"/api/v1/changes":{"parameters":[{"$ref":"#/components/parameters/CorrelationIdHeader"}],"get":{"summary":"Query the change archive","description":"Returns paginated change events from the Apier change archive. Each row records a created/updated/deleted event detected during a periodic upstream poll (Brreg, Altinn, DigDir, Norges Bank, NAV Aa-registeret, and Skatteetaten Tier 2 sub-results — MVA-register + Skatteoppgjør). Use this endpoint to subscribe (via polling) to changes affecting your customers, audit historical mutations, or reconstruct what Apier saw at a given time. Rows are ordered detected_at DESC, id DESC (stable). Pagination is cursor-based — pass the next_cursor returned from each response as the cursor query param to fetch the next page. Cursors are HMAC-signed; tampered cursors return 400 CURSOR_INVALID. Date filters from/to are inclusive RFC 3339 timestamps.","operationId":"queryChanges","tags":["Changes"],"x-required-scope":"read:changes","x-rate-limit-category":"company_data","security":[{"BearerAuth":[]}],"parameters":[{"name":"source","in":"query","required":false,"schema":{"type":"string","enum":["brreg","altinn","digdir","norges_bank","nav","skatteetaten"]},"description":"Filter by upstream data source. Brreg = Brønnøysundregistrene (company data). Altinn = service schemas. DigDir = policy documents. Norges Bank = exchange + interest rates. NAV = Aa-registeret aggregates. Skatteetaten = MVA-register + Skatteoppgjør sub-result diffs (financial figures stripped at the publisher)."},{"name":"entity_type","in":"query","required":false,"schema":{"type":"string","minLength":1,"maxLength":50},"description":"Filter by entity type within the source (e.g., 'company' for brreg, 'schema' for altinn, 'policy' for digdir, 'exchange_rate' for norges_bank, 'company_tier2' for nav)."},{"name":"entity_id","in":"query","required":false,"schema":{"type":"string","minLength":1,"maxLength":100},"description":"Filter by specific entity id (e.g., org_number for brreg, schema_id for altinn)."},{"name":"change_type","in":"query","required":false,"schema":{"type":"string","enum":["created","updated","deleted"]},"description":"Filter by change type."},{"name":"from","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"Inclusive lower bound on detected_at, RFC 3339 with timezone."},{"name":"to","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"Inclusive upper bound on detected_at, RFC 3339 with timezone."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":500,"default":50},"description":"Page size. Defaults to 50, max 500."},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"Opaque pagination cursor from a previous response's next_cursor field. Pass it back as-is. Cursors are HMAC-signed; rotating CHANGES_CURSOR_SECRET invalidates outstanding cursors."}],"responses":{"200":{"description":"Page of change events with cursor for the next page if more rows exist.","headers":{"X-RateLimit-Limit":{"description":"Per-key minute ceiling for the consumer's tier (Category B). Free 30, Starter 150, Professional 300, Enterprise unlimited.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining requests in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix seconds when the window closes.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["success","data","_meta"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["data","pagination"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PublicChangeRow"}},"pagination":{"type":"object","required":["next_cursor","has_more"],"properties":{"next_cursor":{"type":["string","null"],"description":"Opaque cursor for the next page; null when has_more is false."},"has_more":{"type":"boolean","description":"True if more rows exist beyond this page."}}}}},"_meta":{"allOf":[{"$ref":"#/components/schemas/TrustMetadata"},{"type":"object","required":["response_timestamp","response_hash","schema_version"]}]}}},"example":{"success":true,"data":{"data":[{"id":"550e8400-e29b-41d4-a716-446655440000","source":"brreg","entity_type":"company","entity_id":"998877665","change_type":"updated","field_path":"/name","before_value":{"name":"Old Name AS"},"after_value":{"name":"New Name AS"},"diff":[{"op":"replace","path":"/name","value":"New Name AS"}],"detected_at":"2026-04-25T10:00:00.000Z","source_snapshot_id":"6ba7b810-9dad-41d1-80b4-00c04fd430c8"}],"pagination":{"next_cursor":null,"has_more":false}},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-25T12:00:00.000Z","last_verified":"2026-04-25T12:00:00.000Z","source":"apier.no","schema_version":"1.0.5","response_timestamp":"2026-04-25T12:00:00.000Z","response_hash":"sha256:0000000000000000000000000000000000000000000000000000000000000000"}}}}},"400":{"description":"Validation error or tampered cursor.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"VALIDATION_FAILED":{"value":{"success":false,"error_code":"VALIDATION_FAILED","explanation":{"summary":"Invalid query parameters","why":"Forespørselsparametrene mangler eller har feil format.","fix_steps":["Se OpenAPI-spesifikasjonen for /v1/changes for godkjente verdier"],"details":[{"field":"limit","message":"Number must be less than or equal to 500"}]},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-25T12:00:00.000Z","last_verified":"2026-04-25T12:00:00.000Z","source":"apier.no","schema_version":"1.0.5","response_timestamp":"2026-04-25T12:00:00.000Z","response_hash":"sha256:0000000000000000000000000000000000000000000000000000000000000000"}}},"CURSOR_INVALID":{"value":{"success":false,"error_code":"CURSOR_INVALID","explanation":{"summary":"Cursor is invalid or tampered","why":"Paginerings-markøren er ugyldig.","fix_steps":["Bruk next_cursor fra forrige respons som-er, uten endring"]},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-25T12:00:00.000Z","last_verified":"2026-04-25T12:00:00.000Z","source":"apier.no","schema_version":"1.0.5","response_timestamp":"2026-04-25T12:00:00.000Z","response_hash":"sha256:0000000000000000000000000000000000000000000000000000000000000000"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"API key is valid but the consumer's scopes do not include `read:changes`. Add the scope via /v1/admin/keys (operator surface) before retrying.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"scope_insufficient":{"summary":"Key lacks read:changes scope","value":{"success":false,"error_code":"SCOPE_INSUFFICIENT","explanation":{"summary":"API key does not have the required scope","why":"API-nøkkelen mangler scope read:changes. Endepunktet leverer historiske endringer fra arkivet og krever at scopet er aktivert på nøkkelen.","fix_steps":["Be operatøren legge til read:changes-scopet på nøkkelen via /v1/admin/keys"]},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-25T12:00:00.000Z","last_verified":"2026-04-25T12:00:00.000Z","source":"apier.no","schema_version":"1.0.5","response_timestamp":"2026-04-25T12:00:00.000Z","response_hash":"sha256:0000000000000000000000000000000000000000000000000000000000000000"}}}}}}},"429":{"$ref":"#/components/responses/RateLimitExceeded"}}}},"/api/v1/privacy/dsr":{"parameters":[{"$ref":"#/components/parameters/CorrelationIdHeader"}],"post":{"summary":"Data Subject Rights query (GDPR Art 15)","description":"Zero-auth GDPR Article 15 transparency endpoint. Accepts EXACTLY ONE of `name` (case-insensitive substring search across cached signaturrett / prokura / board_members role-holder names) or `org_number` (9-digit Norwegian organisation number). Returns the cached Tier 1 data Apier holds about the requester, with `data_source` / `legal_basis` / `retention_period` annotations on every field — the response is self-describing without the caller needing to hit /privacy or /trust separately.\n\n**No authentication required.** This is a zero-auth surface — Apier does NOT verify that the queried `name` or `org_number` actually belongs to the requester. The endpoint reports what's cached for the submitted query string; Article 15 verification is the requester's responsibility (per Datatilsynet's self-service interpretation for public-register data). Privacy safeguards: (a) **10 requests / 60s sliding window per IP** — prevents bulk-enumeration; (b) **SHA-256-hashed audit-log row per request** — the queried name is never stored in plaintext; (c) **data minimization** — the response is scoped to what Apier actually caches and never enriches with personal data Apier deliberately doesn't store (no birth dates, no addresses, no national identity numbers — Rule 11). The exact response shape varies by query mode: `name` queries return `role_attestations` (one row per (org × role) match — `matched_name` plus the coarse `role` enum `signaturrett` / `prokura` / `board_member`, no other fields about the matched person); `org_number` queries return `company_records` with every public Brønnøysund column the route exposes (name, entity_type, NACE codes, status, municipality, signaturrett, prokura, board_members), each field carrying its own `data_source` / `legal_basis` / `retention_period` triad. **Array-backed columns** (`nace_codes` and the three role-holder lists) are FLATTENED to comma-separated strings on this surface — `DsrField.value` is `string | null` for every key, never an object array, so callers should not expect structured role-holder objects here. The flattening is the documented data-minimisation contract: only the role-holder name appears, never the role label or signing rule.\n\n**Idempotency-Key not supported on this endpoint.** The repo convention is that POST endpoints under `/api/v1/*` support `Idempotency-Key` per CLAUDE.md Rule 5. DSR is the documented exemption: it's a GDPR Article 15 transparency surface, not a consumer-data mutation surface. Every request is a separate disclosure event that MUST land its own audit row for legal-defensibility purposes — deduplicating replays would let a data subject's repeat queries silently merge into one audit entry, eroding the forensic record Datatilsynet may inspect. The append-only audit row IS the side effect, and intentionally-correct duplicate audit rows on replay accurately reflect that the data subject queried twice. Privacy controls (rate limit, hashed name, hashed IP) are independently enforced and protect against bulk-enumeration without needing dedup.\n\n**Correction path:** Brønnøysund is the upstream source of truth. Corrections must be requested at Brønnøysund — they propagate to Apier at the next sync cycle (within 24 hours). Deletion of cached rows is handled by emailing the privacy contact (see response.how_to_request_correction).","operationId":"submitDsrRequest","tags":["Privacy"],"security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"Provide EXACTLY ONE of `name` or `org_number`. Both → 400 VALIDATION_FAILED. Neither → 400 VALIDATION_FAILED. The XOR is enforced by the route's Zod refine and encoded in the OpenAPI schema below via `oneOf` (each branch carries its own `additionalProperties: false`, so the outer schema deliberately omits it to avoid spurious validator failures on the example values).","oneOf":[{"type":"object","additionalProperties":false,"required":["name"],"properties":{"name":{"type":"string","minLength":2,"maxLength":200,"pattern":"(\\S.*){2}","description":"Display-name search across cached signaturrett, prokura, and board_members entries. Case-insensitive substring match (whitespace trimmed before matching). Must contain at least 2 non-whitespace characters — values like `\"a b\"` or `\" Ola \"` are accepted (trim → `\"a b\"` / `\"Ola\"`); `\"  \"` and `\" a \"` are rejected (trimmed length below the floor). The pattern `(\\S.*){2}` mirrors the route's `z.string().trim().min(2)` semantics exactly. Mutually exclusive with `org_number`."}}},{"type":"object","additionalProperties":false,"required":["org_number"],"properties":{"org_number":{"type":"string","pattern":"^\\d{9}$","description":"9-digit Norwegian organisation number. Returns the company_records row Apier holds about that org. Mutually exclusive with `name`. Note: org_number for AS / ANS / DA is a business identifier (not personal data); for ENK (enkeltpersonforetak) it is personal data per Datatilsynet's interpretation — this endpoint serves both cases identically."}}}]},"examples":{"by_name":{"summary":"Query by display name","value":{"name":"Ola Nordmann"}},"by_org_number":{"summary":"Query by org_number","value":{"org_number":"998877665"}}}}}},"responses":{"200":{"description":"Annotated DSR response with `data_source`, `legal_basis`, and `retention_period` on every field.","headers":{"Cache-Control":{"description":"Always `no-store` — DSR responses contain personal data and must not be cached by any intermediary (browser, CDN, corporate proxy). Set on every response path the route emits (200 / 400 / 429 / 500); only the 200 response documents the header here, matching the precedent in this spec where the contract is published once on the canonical-success path. Round 17 introduced this invariant via the route's `withNoStore` helper.","schema":{"type":"string","enum":["no-store"]}}},"content":{"application/json":{"schema":{"type":"object","required":["success","data","_meta"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["query","results","privacy_policy_url","how_to_request_correction"],"properties":{"query":{"type":"object","description":"Echoes the request — exactly one of name or org_number, mirrored as a `oneOf` so generated clients see the same XOR shape the request body enforces. The echoed value is post-Zod (whitespace-trimmed for `name`, character-validated for `org_number`); a client that submitted `\" Ola \"` will see `\"Ola\"` here.","oneOf":[{"type":"object","additionalProperties":false,"required":["name"],"properties":{"name":{"type":"string","description":"Trimmed name as the route saw it (the request's `name` after `z.string().trim()` ran). Always present when the requester queried by name."}}},{"type":"object","additionalProperties":false,"required":["org_number"],"properties":{"org_number":{"type":"string","pattern":"^\\d{9}$","description":"9-digit Norwegian organisation number echoed verbatim from the request. Always present when the requester queried by org_number."}}}]},"results":{"type":"object","required":["company_records","role_attestations","truncated"],"properties":{"company_records":{"type":"array","description":"Populated when `org_number` was queried. Each row carries every public-facing field annotated with the GDPR transparency triad.","items":{"$ref":"#/components/schemas/DsrCompanyRecord"}},"role_attestations":{"type":"array","description":"Populated when `name` was queried. One attestation per (org × role) match. Each role bucket is scanned independently — a name appearing in signaturrett does NOT cross-contaminate the prokura attestation list.","items":{"$ref":"#/components/schemas/DsrRoleAttestation"}},"truncated":{"type":"boolean","description":"True when the candidate-row scan hit the per-column LIMIT (100) — Article 15 responses are required to be complete OR honestly indicate when they are not. Pagination support is a follow-up; today the consumer should refine the query (e.g. add a more specific surname) when this flag is true."}}},"privacy_policy_url":{"type":"string","format":"uri","description":"Public privacy policy URL. Built from NEXT_PUBLIC_APP_URL + `/privacy`."},"how_to_request_correction":{"type":"string","description":"Static guidance — corrections happen at Brønnøysund (the upstream source); deletions of cached rows are handled by emailing the privacy contact."},"expanded_categories_require_org_number":{"type":"boolean","enum":[true],"description":"PR-028b + PR-028c — present (and ALWAYS `true`) when only `name` was supplied. Round-5 fix (CodeRabbit Minor): schema locked to `enum: [true]` so generated clients align with the runtime contract — the field is never emitted with `false`; the absence-on-org_number-queries case is signalled by the property being missing entirely, not by `false`. The six expanded categories — `delegations`, `evaluation_snapshots`, `receipts` (PR-028b), `provenance_log`, `changes`, `api_audit_log` (PR-028c Amendment 001) — are org-scoped, not name-scoped: a Norwegian person can hold a role on multiple organisations, so disambiguation requires an org_number. The data subject can either provide their org_number directly or first run the name-lookup branch and re-request with a returned org_number from `results.role_attestations[].org_number`."},"delegations":{"x-data-classification":"personal-data","type":"object","description":"PR-028b — Amendment 59 / PR-016 append-only, time-bounded Altinn System User authorisations granted by the org_number's authorised signatories. Present on org_number queries. Contains `system_user_id` (an Altinn-side identifier — operator-level personal data per Datatilsynet's interpretation of pseudonymous identifiers) and `scopes` (capability strings). The agent's WHY-this-PR-exists answer: PR-016's Amendment 59 tightening made delegations append-only and time-bounded; the data subject is entitled to know what authorisations are open against their org under GDPR Art 15.","required":["data_source","legal_basis","retention_period","records","hasMore","truncated_due_to_timeout"],"properties":{"data_source":{"type":"string"},"legal_basis":{"type":"string"},"retention_period":{"type":"string"},"records":{"type":"array","items":{"x-data-classification":"personal-data","type":"object","required":["id","org_number","system_user_id","scopes","valid_from","valid_until","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"org_number":{"type":"string","pattern":"^\\d{9}$"},"system_user_id":{"type":"string"},"scopes":{"type":"array","items":{"type":"string"}},"valid_from":{"type":"string","format":"date-time"},"valid_until":{"type":["string","null"],"format":"date-time","description":"Null while the authorisation is open-ended."},"created_at":{"type":"string","format":"date-time"}}}},"hasMore":{"type":"boolean","description":"Pagination signal — true when the row count exceeded the per-helper LIMIT (100). Today the consumer should narrow the query (or wait for cursor pagination)."},"truncated_due_to_timeout":{"type":"boolean","description":"True when the helper hit its 5 s budget and the records array is empty as a safety measure. The 200 status is preserved either way so the data subject sees a uniform response shape."}}},"evaluation_snapshots":{"x-data-classification":"personal-data","type":"object","description":"PR-028b — Amendment 59 Invariant 6 / PR-047b: forensic record of every Rulebook evaluation. The agent's WHY: a data subject may have triggered an evaluation with personal data on input — they're entitled to see the audit trail under GDPR Art 15. **Critical data-minimisation contract (Rule 11):** only `inputs_hash` is exposed; the raw `inputs` JSONB is NEVER returned via DSR. The hash is enough for the data subject to match an evaluation against an audit-log row from the same correlation_id; the raw `inputs` recovery is a separate authenticated path through Apier support.","required":["data_source","legal_basis","retention_period","records","hasMore","truncated_due_to_timeout"],"properties":{"data_source":{"type":"string"},"legal_basis":{"type":"string"},"retention_period":{"type":"string"},"records":{"type":"array","items":{"x-data-classification":"personal-data","type":"object","required":["id","correlation_id","timestamp","endpoint","rulebook_version","schema_version","outcome","inputs_hash"],"properties":{"id":{"type":"string","format":"uuid"},"correlation_id":{"type":"string","format":"uuid"},"timestamp":{"type":"string","format":"date-time"},"endpoint":{"type":"string"},"rulebook_version":{"type":"string"},"schema_version":{"type":"string"},"outcome":{"description":"JSONB — evaluator outcome shape varies by endpoint."},"inputs_hash":{"type":"string","description":"SHA-256 hash of canonicalized inputs — the raw inputs JSONB is NEVER exposed via DSR (data minimization, GDPR Art 5)."}}}},"hasMore":{"type":"boolean"},"truncated_due_to_timeout":{"type":"boolean"}}},"receipts":{"x-data-classification":"personal-data","type":"object","description":"PR-028b — Amendment 59 Invariant 5 / PR-075: signed submission receipts for filings made via /v1/actions/execute. The agent's WHY: government responses (Maskinporten / Altinn / Skatteetaten / NAV) are preserved verbatim for audit defensibility; the data subject is entitled to see the response payload that was attached to a filing made on their org. The full payload above 64 KiB UTF-8 BYTES (note: bytes, not characters) spills to a separate authenticated bucket — `government_response_truncated: true` signals this case, and `government_response_hash` lets the data subject verify authenticity without DSR fetching the spill row.","required":["data_source","legal_basis","retention_period","records","hasMore","truncated_due_to_timeout"],"properties":{"data_source":{"type":"string"},"legal_basis":{"type":"string"},"retention_period":{"type":"string"},"records":{"type":"array","items":{"x-data-classification":"personal-data","type":"object","required":["id","altinn_receipt_id","audit_log_id","created_at","filing_type","rule_version","government_response_raw","government_response_truncated","government_response_hash"],"properties":{"id":{"type":"string","format":"uuid"},"altinn_receipt_id":{"type":"string"},"audit_log_id":{"type":"string","format":"uuid"},"created_at":{"type":"string","format":"date-time"},"filing_type":{"type":"string"},"rule_version":{"type":"string"},"government_response_raw":{"description":"JSONB — first-64-KiB UTF-8-byte projection when truncated; full payload otherwise."},"government_response_truncated":{"type":"boolean"},"government_response_hash":{"type":"string","pattern":"^sha256:[0-9a-f]{64}$"}}}},"hasMore":{"type":"boolean"},"truncated_due_to_timeout":{"type":"boolean"}}},"provenance_log":{"x-data-classification":"personal-data","type":"object","description":"PR-028c — Amendment 001 Invariant 7 / PR-010b: SHA-256 hash of every API response served to or about the org_number's records. Linked via audit_log.correlation_id; provenance_log itself has no org_number column. The agent's WHY: a data subject can verify what bytes Apier shipped about them by matching response_hash against an offline copy. Each row's correlation_id links forensically to other audit-chain rows (audit_log, evaluation_snapshots) for the same request — NOT the same correlation_id as this DSR request.","required":["data_source","legal_basis","retention_period","records","hasMore","truncated_due_to_timeout"],"properties":{"data_source":{"type":"string"},"legal_basis":{"type":"string"},"retention_period":{"type":"string"},"records":{"type":"array","items":{"x-data-classification":"personal-data","type":"object","required":["id","correlation_id","endpoint","status_code","rulebook_version","schema_version","source_snapshot_id","response_timestamp","response_hash","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"correlation_id":{"type":"string","format":"uuid"},"endpoint":{"type":"string"},"status_code":{"type":"integer"},"rulebook_version":{"type":["string","null"]},"schema_version":{"type":"string"},"source_snapshot_id":{"type":["string","null"]},"response_timestamp":{"type":"string","format":"date-time"},"response_hash":{"type":"string","pattern":"^sha256:[0-9a-f]{64}$"},"created_at":{"type":"string","format":"date-time"}}}},"hasMore":{"type":"boolean"},"truncated_due_to_timeout":{"type":"boolean"},"truncated_due_to_size":{"type":"boolean","description":"PR-028c — true when the helper's Step A (audit_log → correlation_ids) hit its 200-distinct-correlation_id cap. The data subject's MOST RECENT 200 distinct correlation_ids were used; older rows exist beyond the cap. Round-5 reduction from 5000 → 200 (BugBot HIGH fix): keeps Step B's `.in()` IN-list under PostgREST's URL length limit; future hardening via a SECURITY DEFINER RPC can lift the cap. Distinct from `hasMore` (page-100 cap on the final records list) and `truncated_due_to_timeout` (5 s helper budget)."}}},"changes":{"x-data-classification":"personal-data","type":"object","description":"PR-028c — Amendment 001 §3.B / PR-023b: append-only archive of upstream registry changes (Brønnøysund / Altinn / Digdir / Norges Bank) for the org_number's business entity. Filtered at the helper boundary to entity_type IN ('company') so non-org-keyed adapter rows (schema / policy / exchange_rate) cannot leak via 9-digit string collisions on entity_id. The agent's WHY: every detected change to the data subject's org appears here with its before/after JSONB projection. Personal data CAN appear in before_value / after_value / diff when a director or signatory changes (the publisher already strips raw personal fields at the adapter boundary; what remains is the tracked-field projection). Each row's correlation_id links to the audit_log row that triggered the publisher.","required":["data_source","legal_basis","retention_period","records","hasMore","truncated_due_to_timeout"],"properties":{"data_source":{"type":"string"},"legal_basis":{"type":"string"},"retention_period":{"type":"string"},"records":{"type":"array","items":{"x-data-classification":"personal-data","type":"object","required":["id","source","entity_type","entity_id","change_type","field_path","before_value","after_value","diff","detected_at","source_snapshot_id","correlation_id"],"properties":{"id":{"type":"string","format":"uuid"},"source":{"type":"string","enum":["brreg"],"description":"Always `brreg` for rows surfaced by DSR — only Brønnøysund-sourced changes carry an org_number-shaped `entity_id` (the helper filters by `entity_type IN ('company')`, which only Brreg emits). Schema-locked via single-element enum so generated clients can rely on the literal value (round-5 fix — CodeRabbit Minor)."},"entity_type":{"type":"string","enum":["company"],"description":"Always 'company' for rows surfaced by DSR — the helper filters by entity_type IN ('company'). Schema-locked via enum so generated clients can rely on the literal value."},"entity_id":{"type":"string","pattern":"^\\d{9}$"},"change_type":{"type":"string","enum":["created","updated","deleted"]},"field_path":{"type":["string","null"]},"before_value":{"x-data-classification":"potentially-personal-data","description":"JSONB — tracked-field projection. Can contain personal data when a director/signatory changes."},"after_value":{"x-data-classification":"potentially-personal-data","description":"JSONB — tracked-field projection. Can contain personal data when a director/signatory changes."},"diff":{"x-data-classification":"potentially-personal-data","description":"JSONB — JSON-Patch-style diff. Can contain personal data when a director/signatory changes."},"detected_at":{"type":"string","format":"date-time"},"source_snapshot_id":{"type":"string"},"correlation_id":{"type":["string","null"],"format":"uuid","description":"Forensic linkage to the audit_log row that triggered the publisher run. NOT the same correlation_id as the current DSR request."}}}},"hasMore":{"type":"boolean"},"truncated_due_to_timeout":{"type":"boolean"}}},"api_audit_log":{"x-data-classification":"personal-data","type":"object","description":"PR-028c — Amendment 001 §3.C / PR-013b / Rule 30: append-only scope-check audit. Returns rows associated with API keys held by the data subject's consumer. **Current schema:** api_consumers has no org_number column, so the helper cannot link records to an org_number; the response carries an empty records array and an honest data_source explanation. **Future schema:** when api_consumers gains an org_number column, the helper rewrites to perform the three-step JOIN (api_consumers → api_keys → api_audit_log) with explicit .select('id') on the api_keys intermediate so api_keys.key_hash is NEVER selected even for internal IN-list use. **`key_hash` is INTENTIONALLY ABSENT from the record schema** (three-layer defense: helper allow-list, type system, paired Rule-11 positive-control test).","required":["data_source","legal_basis","retention_period","records","hasMore","truncated_due_to_timeout"],"properties":{"data_source":{"type":"string"},"legal_basis":{"type":"string"},"retention_period":{"type":"string"},"records":{"type":"array","items":{"x-data-classification":"personal-data","type":"object","required":["id","api_key_id","endpoint","scope_required","scope_granted","correlation_id","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"api_key_id":{"type":"string","format":"uuid"},"endpoint":{"type":"string"},"scope_required":{"type":"string","x-data-classification":"metadata","description":"The scope the endpoint required at the time of the call (e.g. 'read:brreg')."},"scope_granted":{"type":"boolean","x-data-classification":"metadata","description":"Whether the API key satisfied the required scope."},"correlation_id":{"type":"string","format":"uuid"},"created_at":{"type":"string","format":"date-time"}}}},"hasMore":{"type":"boolean"},"truncated_due_to_timeout":{"type":"boolean"},"truncated_due_to_size":{"type":"boolean","description":"PR-028c — true when the helper's Step A consumer-cap or intermediate IN-list cap triggers (CASE A path; never set in current CASE B implementation)."}}}}},"_meta":{"$ref":"#/components/schemas/TrustMetadata"}}}}}},"400":{"description":"Validation error. Body returned `VALIDATION_FAILED` for any of: (a) both `name` AND `org_number` provided; (b) neither provided; (c) `org_number` not a 9-digit string; (d) `name` falling below 2 non-whitespace characters after trimming (`\"  \"`, `\" a \"`, etc.); (e) `name` longer than 200 characters; (f) request body not valid JSON; (g) unknown top-level field present (Zod strictObject — Rule 26 No Silent Fallbacks). Per-issue `explanation.details` enumerate each failed field for client diagnostics.","headers":{"Cache-Control":{"description":"Always `no-store` — privacy-surface invariant; see the 200 response's documentation.","schema":{"type":"string","enum":["no-store"]}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"both_provided":{"summary":"Both fields provided (XOR violation)","value":{"success":false,"error_code":"VALIDATION_FAILED","explanation":{"summary":"Invalid request body","why":"Forespørselen må inneholde nøyaktig ett av feltene name eller org_number.","fix_steps":["Send {\"name\": \"<navn>\"} eller {\"org_number\": \"<9 siffer>\"} — ikke begge"]},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-25T12:00:00.000Z","last_verified":"2026-04-25T12:00:00.000Z","source":"apier.no","schema_version":"1.0.5","response_timestamp":"2026-04-25T12:00:00.000Z","response_hash":"sha256:0000000000000000000000000000000000000000000000000000000000000000"}}},"name_too_short_after_trim":{"summary":"name fell below 2 non-whitespace characters after trim","value":{"success":false,"error_code":"VALIDATION_FAILED","explanation":{"summary":"Invalid request body","why":"Forespørselen må inneholde nøyaktig ett av feltene name eller org_number.","fix_steps":["Send {\"name\": \"<navn>\"} eller {\"org_number\": \"<9 siffer>\"} — ikke begge"],"details":[{"field":"name","message":"String must contain at least 2 character(s)"}]},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-25T12:00:00.000Z","last_verified":"2026-04-25T12:00:00.000Z","source":"apier.no","schema_version":"1.0.5","response_timestamp":"2026-04-25T12:00:00.000Z","response_hash":"sha256:0000000000000000000000000000000000000000000000000000000000000000"}}},"body_not_valid_json":{"summary":"Request body is not valid JSON","value":{"success":false,"error_code":"VALIDATION_FAILED","explanation":{"summary":"Request body is not valid JSON","fix_steps":["POST application/json with a body of {\"name\": \"...\"} or {\"org_number\": \"123456789\"}"]},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-25T12:00:00.000Z","last_verified":"2026-04-25T12:00:00.000Z","source":"apier.no","schema_version":"1.0.5","response_timestamp":"2026-04-25T12:00:00.000Z","response_hash":"sha256:0000000000000000000000000000000000000000000000000000000000000000"}}}}}}},"429":{"description":"Per-IP rate limit reached (10 requests / 60-second sliding window). Retry-After header indicates the seconds until the oldest in-window timestamp expires. Limit is privacy-protected, intentionally lower than Category A's 1000/min — DSR is an Article 15 transparency surface, not a discovery one. Both `Retry-After` and `Cache-Control` headers coexist on this response.","headers":{"Retry-After":{"description":"Seconds to wait before retrying. Integer, always >= 1.","schema":{"type":"integer","minimum":1}},"Cache-Control":{"description":"Always `no-store` — privacy-surface invariant; see the 200 response's documentation.","schema":{"type":"string","enum":["no-store"]}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"rate_limit":{"summary":"Per-IP limit hit","value":{"success":false,"error_code":"RATE_LIMIT_EXCEEDED","explanation":{"summary":"Rate limit exceeded","why":"Grense per minutt for /api/v1/privacy/dsr er nådd (10 forespørsler per IP). Vent 30 sekunder før du prøver igjen.","fix_steps":["Vent 30 sekunder og prøv igjen"]},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-25T12:00:00.000Z","last_verified":"2026-04-25T12:00:00.000Z","source":"apier.no","schema_version":"1.0.5","response_timestamp":"2026-04-25T12:00:00.000Z","response_hash":"sha256:0000000000000000000000000000000000000000000000000000000000000000"}}}}}}},"500":{"description":"Server error during the cached-data lookup or the response-shaping step. The route's outer try/catch funnels every unexpected throw into a structured `SERVER_ERROR` envelope (Rule 24 — no stack traces, no raw DB errors). Distinct error codes (`SERVER_ERROR` for both lookup failures and the catch-all path) keep the public contract narrow; the Sentry-side `error_code` (`DSR_LOOKUP_FAILED` / `DSR_UNEXPECTED`) gives operators the granularity for triage.","headers":{"Cache-Control":{"description":"Always `no-store` — privacy-surface invariant; see the 200 response's documentation.","schema":{"type":"string","enum":["no-store"]}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"lookup_failed":{"summary":"Lookup against the companies table failed","value":{"success":false,"error_code":"SERVER_ERROR","explanation":{"summary":"Failed to look up the requested record"},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-25T12:00:00.000Z","last_verified":"2026-04-25T12:00:00.000Z","source":"apier.no","schema_version":"1.0.5","response_timestamp":"2026-04-25T12:00:00.000Z","response_hash":"sha256:0000000000000000000000000000000000000000000000000000000000000000"}}},"unexpected":{"summary":"Unexpected exception in the response-shaping path","value":{"success":false,"error_code":"SERVER_ERROR","explanation":{"summary":"Failed to process the privacy request"},"_meta":{"rulebook_version":"1.0.0","data_freshness":"2026-04-25T12:00:00.000Z","last_verified":"2026-04-25T12:00:00.000Z","source":"apier.no","schema_version":"1.0.5","response_timestamp":"2026-04-25T12:00:00.000Z","response_hash":"sha256:0000000000000000000000000000000000000000000000000000000000000000"}}}}}}}}}},"/api/v1/subscriptions":{"parameters":[{"$ref":"#/components/parameters/CorrelationIdHeader"}],"post":{"summary":"Create a webhook subscription","description":"Creates a Pro-tier change-detection webhook subscription. The plaintext webhook secret is returned ONCE in the 201 response and never again — store it client-side. Apier signs every delivery with HMAC-SHA256 over `<timestamp>.<body>` (Stripe-style header `t=<unix>,v1=<hex>` on `X-Apier-Signature`). Webhook URLs are SSRF-validated at create time AND at every delivery; a 7-attempt exponential-backoff schedule (initial try plus retries at 1m / 5m / 15m / 1h / 6h / 24h) is exhausted before a delivery is abandoned. After 6 consecutive 4xx responses (excluding 408 / 429 — receivers asking us to back off don't burn the counter) the subscription auto-disables. Outbound delivery contract: receivers should expect `WebhookDeliveryHeaders` plus a JSON body matching `WebhookDeliveryEvent` (both in components.schemas).","operationId":"createSubscription","tags":["Subscriptions"],"x-required-scope":"subscribe:webhooks","x-rate-limit-category":"company_data","security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionCreateRequest"}}}},"responses":{"201":{"description":"Subscription created. The plaintext `webhook_secret` is returned exactly once.","headers":{"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"description":"Always `</openapi.json>; rel=\"service-desc\"` per CLAUDE.md Rule 4 — every /api/v1/* response carries this self-discovery pointer.","schema":{"type":"string"}},"X-RateLimit-Limit":{"description":"Per-key minute ceiling for the consumer's tier (Category B). Free 30, Starter 150, Professional 300, Enterprise unlimited.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining requests in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix seconds when the window closes.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["success","data","_meta"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"$ref":"#/components/schemas/SubscriptionCreateResponse"},"_meta":{"$ref":"#/components/schemas/TrustMetadata"}}}}}},"400":{"description":"Validation error, SSRF-blocked URL, or active-subscription cap reached.","headers":{"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"description":"Always `</openapi.json>; rel=\"service-desc\"` per CLAUDE.md Rule 4.","schema":{"type":"string"}},"X-RateLimit-Limit":{"description":"Per-key minute ceiling for the consumer's tier (Category B). Echoed on 4xx/5xx so retry logic has the same observability as 2xx/429 paths.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining requests in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix seconds when the window closes.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"API key lacks `subscribe:webhooks` scope.","headers":{"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"description":"Always `</openapi.json>; rel=\"service-desc\"` per CLAUDE.md Rule 4.","schema":{"type":"string"}},"X-RateLimit-Limit":{"description":"Per-key minute ceiling for the consumer's tier (Category B). Echoed on 4xx/5xx so retry logic has the same observability as 2xx/429 paths.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining requests in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix seconds when the window closes.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"$ref":"#/components/responses/RateLimitExceeded"},"500":{"description":"Internal server error — unexpected Supabase / RPC / encryption failure. Body follows the structured ApiError envelope (CLAUDE.md Rule 24 — no internal details leak).","headers":{"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"description":"Always `</openapi.json>; rel=\"service-desc\"` per CLAUDE.md Rule 4.","schema":{"type":"string"}},"X-RateLimit-Limit":{"description":"Per-key minute ceiling for the consumer's tier (Category B). Echoed on 4xx/5xx so retry logic has the same observability as 2xx/429 paths.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining requests in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix seconds when the window closes.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"503":{"description":"Server misconfigured — `WEBHOOK_SECRET_ENC_KEY` missing.","headers":{"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"description":"Always `</openapi.json>; rel=\"service-desc\"` per CLAUDE.md Rule 4.","schema":{"type":"string"}},"X-RateLimit-Limit":{"description":"Per-key minute ceiling for the consumer's tier (Category B). Echoed on 4xx/5xx so retry logic has the same observability as 2xx/429 paths.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining requests in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix seconds when the window closes.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}},"get":{"summary":"List active webhook subscriptions","description":"Lists active subscriptions for the calling consumer. Secret material (hash AND ciphertext) is never returned in any form — only metadata. Inactive subscriptions are not included.","operationId":"listSubscriptions","tags":["Subscriptions"],"x-required-scope":"subscribe:webhooks","x-rate-limit-category":"company_data","security":[{"BearerAuth":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"description":"Accepted for forward compatibility but IGNORED at v1. The handler always returns up to the active-subscription hard cap (10) — silently truncating with a `?limit=1` request would leave the remaining rows unreachable. The validator still rejects out-of-range values so generated clients fail-fast on bad input. Will become meaningful when active-subscription caps rise and pagination is added."}],"responses":{"200":{"description":"All active subscriptions belonging to the caller. Not paginated — the per-consumer cap of 10 ensures the response always fits in one page.","headers":{"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"description":"Always `</openapi.json>; rel=\"service-desc\"` per CLAUDE.md Rule 4.","schema":{"type":"string"}},"X-RateLimit-Limit":{"description":"Per-key minute ceiling for the consumer's tier (Category B).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining requests in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix seconds when the window closes.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["success","data","_meta"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"$ref":"#/components/schemas/SubscriptionListResponse"},"_meta":{"$ref":"#/components/schemas/TrustMetadata"}}}}}},"400":{"description":"Validation failed — `limit` query parameter outside allowed range (1-200), or other parameter validation error.","headers":{"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"description":"Always `</openapi.json>; rel=\"service-desc\"` per CLAUDE.md Rule 4.","schema":{"type":"string"}},"X-RateLimit-Limit":{"description":"Per-key minute ceiling for the consumer's tier (Category B). Echoed on 4xx/5xx so retry logic has the same observability as 2xx/429 paths.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining requests in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix seconds when the window closes.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"API key lacks `subscribe:webhooks` scope.","headers":{"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"description":"Always `</openapi.json>; rel=\"service-desc\"` per CLAUDE.md Rule 4.","schema":{"type":"string"}},"X-RateLimit-Limit":{"description":"Per-key minute ceiling for the consumer's tier (Category B). Echoed on 4xx/5xx so retry logic has the same observability as 2xx/429 paths.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining requests in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix seconds when the window closes.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"$ref":"#/components/responses/RateLimitExceeded"},"500":{"description":"Internal server error — unexpected Supabase / RPC failure. Body follows the structured ApiError envelope.","headers":{"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"description":"Always `</openapi.json>; rel=\"service-desc\"` per CLAUDE.md Rule 4.","schema":{"type":"string"}},"X-RateLimit-Limit":{"description":"Per-key minute ceiling for the consumer's tier (Category B). Echoed on 4xx/5xx so retry logic has the same observability as 2xx/429 paths.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining requests in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix seconds when the window closes.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/subscriptions/{id}":{"parameters":[{"$ref":"#/components/parameters/CorrelationIdHeader"}],"delete":{"summary":"Delete a webhook subscription","description":"Soft-deletes a subscription (active=false, deactivation_reason='user_deleted'). Idempotent — subsequent DELETEs return 200. A subscription owned by a different consumer returns 404 (the existence-leak vector is closed by treating cross-consumer access as not-found rather than forbidden).","operationId":"deleteSubscription","tags":["Subscriptions"],"x-required-scope":"subscribe:webhooks","x-rate-limit-category":"company_data","security":[{"BearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Subscription deleted (or already inactive — idempotent).","headers":{"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"description":"Always `</openapi.json>; rel=\"service-desc\"` per CLAUDE.md Rule 4.","schema":{"type":"string"}},"X-RateLimit-Limit":{"description":"Per-key minute ceiling for the consumer's tier (Category B).","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining requests in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix seconds when the window closes.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["success","data","_meta"],"properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","required":["id","active"],"properties":{"id":{"type":"string","format":"uuid","description":"Echoes back the deleted subscription id so the caller can confirm which row was deactivated."},"active":{"type":"boolean","enum":[false],"description":"Always false on the success path — DELETE soft-deletes by flipping `active` to false; the row itself stays in the database (preserving the FK to webhook_deliveries audit rows)."}}},"_meta":{"$ref":"#/components/schemas/TrustMetadata"}}}}}},"400":{"description":"Invalid UUID id.","headers":{"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"description":"Always `</openapi.json>; rel=\"service-desc\"` per CLAUDE.md Rule 4.","schema":{"type":"string"}},"X-RateLimit-Limit":{"description":"Per-key minute ceiling for the consumer's tier (Category B). Echoed on 4xx/5xx so retry logic has the same observability as 2xx/429 paths.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining requests in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix seconds when the window closes.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"API key lacks `subscribe:webhooks` scope.","headers":{"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"description":"Always `</openapi.json>; rel=\"service-desc\"` per CLAUDE.md Rule 4.","schema":{"type":"string"}},"X-RateLimit-Limit":{"description":"Per-key minute ceiling for the consumer's tier (Category B). Echoed on 4xx/5xx so retry logic has the same observability as 2xx/429 paths.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining requests in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix seconds when the window closes.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Subscription not found OR belongs to a different consumer.","headers":{"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"description":"Always `</openapi.json>; rel=\"service-desc\"` per CLAUDE.md Rule 4.","schema":{"type":"string"}},"X-RateLimit-Limit":{"description":"Per-key minute ceiling for the consumer's tier (Category B). Echoed on 4xx/5xx so retry logic has the same observability as 2xx/429 paths.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining requests in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix seconds when the window closes.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"$ref":"#/components/responses/RateLimitExceeded"},"500":{"description":"Internal server error — unexpected Supabase / RPC failure. Body follows the structured ApiError envelope.","headers":{"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"description":"Always `</openapi.json>; rel=\"service-desc\"` per CLAUDE.md Rule 4.","schema":{"type":"string"}},"X-RateLimit-Limit":{"description":"Per-key minute ceiling for the consumer's tier (Category B). Echoed on 4xx/5xx so retry logic has the same observability as 2xx/429 paths.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining requests in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix seconds when the window closes.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/v1/sandbox/company/{org}/context":{"parameters":[{"name":"org","in":"path","required":true,"schema":{"type":"string","pattern":"^\\d{9}$"},"description":"9-digit Norwegian organisation number. Sandbox uses a format-only validator (no Brønnøysund MOD-11 checksum); reserved synthetic test orgs (999000XXX) and reserved error orgs (999000901-904) are accepted by design — see DECISIONS.md PR-074 / Sandbox Production Schema Divergence.","examples":{"test_tier_1":{"value":"999000001","summary":"Tier 1 happy-path AS"},"test_tier_1_2":{"value":"999000003","summary":"Tier 1+2 happy-path AS"},"error_auth_missing":{"value":"999000901","summary":"Reserved error: AUTH_MISSING_DELEGATION"}}},{"$ref":"#/components/parameters/CorrelationIdHeader"}],"get":{"summary":"Sandbox mirror of /api/v1/company/{org}/context (zero-auth)","description":"Zero-auth sandbox mirror of `GET /api/v1/company/{org}/context`. Returns synthetic Brønnøysund-shaped data deterministically — same input yields the same response (excluding `_meta.response_timestamp`). Reserved test orgs, reserved error orgs, and `?simulate_error=<code>` are documented on `/api/v1/capabilities`. CORS is open (`Access-Control-Allow-Origin: *`) so browser-based agents can fetch from any origin. NO authentication required (`security: []` overrides any global Bearer requirement). Synthetic data only.","tags":["Sandbox"],"operationId":"sandboxCompanyContext","security":[{"BearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/SandboxSimulateError"}],"responses":{"200":{"$ref":"#/components/responses/SandboxContextSuccess"},"400":{"$ref":"#/components/responses/SandboxError"},"401":{"$ref":"#/components/responses/SandboxError"},"403":{"$ref":"#/components/responses/SandboxError"},"404":{"$ref":"#/components/responses/SandboxError"},"500":{"$ref":"#/components/responses/SandboxError"}}},"options":{"summary":"CORS preflight for the sandbox context endpoint","description":"Returns 204 with the standard sandbox CORS headers. Browser-based agents using non-simple requests trigger this preflight; simple GET requests skip it.","tags":["Sandbox"],"operationId":"sandboxCompanyContextOptions","security":[],"responses":{"204":{"$ref":"#/components/responses/SandboxOptionsPreflight"}}}},"/api/v1/sandbox/company/{org}/obligations":{"parameters":[{"name":"org","in":"path","required":true,"schema":{"type":"string","pattern":"^\\d{9}$"},"description":"9-digit Norwegian organisation number. See sandbox context endpoint for reserved-org documentation."},{"$ref":"#/components/parameters/CorrelationIdHeader"}],"get":{"summary":"Sandbox mirror of /api/v1/company/{org}/obligations (zero-auth)","description":"Zero-auth sandbox mirror of `GET /api/v1/company/{org}/obligations`. See sandbox context endpoint for the full sandbox contract.","tags":["Sandbox"],"operationId":"sandboxCompanyObligations","security":[{"BearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/SandboxSimulateError"}],"responses":{"200":{"$ref":"#/components/responses/SandboxObligationsSuccess"},"400":{"$ref":"#/components/responses/SandboxError"},"401":{"$ref":"#/components/responses/SandboxError"},"403":{"$ref":"#/components/responses/SandboxError"},"404":{"$ref":"#/components/responses/SandboxError"},"500":{"$ref":"#/components/responses/SandboxError"}}},"options":{"summary":"CORS preflight for the sandbox obligations endpoint","description":"Returns 204 with the standard sandbox CORS headers. See sandbox context endpoint OPTIONS for the full preflight contract.","tags":["Sandbox"],"operationId":"sandboxCompanyObligationsOptions","security":[],"responses":{"204":{"$ref":"#/components/responses/SandboxOptionsPreflight"}}}},"/api/v1/sandbox/company/{org}/deadlines":{"parameters":[{"name":"org","in":"path","required":true,"schema":{"type":"string","pattern":"^\\d{9}$"},"description":"9-digit Norwegian organisation number. See sandbox context endpoint for reserved-org documentation."},{"$ref":"#/components/parameters/CorrelationIdHeader"}],"get":{"summary":"Sandbox mirror of /api/v1/company/{org}/deadlines (zero-auth)","description":"Zero-auth sandbox mirror of `GET /api/v1/company/{org}/deadlines`. Production accepts `?from_date=` and `?horizon_months=`; sandbox accepts the same params for client-shape parity but returns the fixture's static deadline list verbatim — filtering would require synthesising a calendar engine in the mock layer, which the deterministic-fixture model rejects.","tags":["Sandbox"],"operationId":"sandboxCompanyDeadlines","security":[{"BearerAuth":[]}],"parameters":[{"name":"from_date","in":"query","required":false,"schema":{"type":"string"},"description":"Accepted for production-shape parity. Production also uses `type: string` at the wire layer (accepts bare `YYYY-MM-DD` or full ISO 8601 timestamp). Sandbox does NOT enforce the date-format constraint at runtime and returns the static fixture data regardless. Agents wanting calendar-logic verification must use the production endpoint with a test consumer."},{"name":"horizon_months","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":60},"description":"Schema mirrors production exactly (integer 1-60). The schema is the contract clients honour at codegen time; URLSearchParams serialises the integer back to a string on the wire, which both sandbox and production accept. Sandbox runtime does NOT validate the value and returns the static fixture regardless — agents wanting calendar-logic verification must use the production endpoint with a test consumer."},{"$ref":"#/components/parameters/SandboxSimulateError"}],"responses":{"200":{"$ref":"#/components/responses/SandboxDeadlinesSuccess"},"400":{"$ref":"#/components/responses/SandboxError"},"401":{"$ref":"#/components/responses/SandboxError"},"403":{"$ref":"#/components/responses/SandboxError"},"404":{"$ref":"#/components/responses/SandboxError"},"500":{"$ref":"#/components/responses/SandboxError"}}},"options":{"summary":"CORS preflight for the sandbox deadlines endpoint","description":"Returns 204 with the standard sandbox CORS headers. See sandbox context endpoint OPTIONS for the full preflight contract.","tags":["Sandbox"],"operationId":"sandboxCompanyDeadlinesOptions","security":[],"responses":{"204":{"$ref":"#/components/responses/SandboxOptionsPreflight"}}}},"/api/v1/sandbox/company/{org}/summary":{"parameters":[{"name":"org","in":"path","required":true,"schema":{"type":"string","pattern":"^\\d{9}$"},"description":"9-digit Norwegian organisation number. See sandbox context endpoint for reserved-org documentation."},{"$ref":"#/components/parameters/CorrelationIdHeader"}],"get":{"summary":"Sandbox mirror of /api/v1/company/{org}/summary (zero-auth)","description":"Zero-auth sandbox mirror of `GET /api/v1/company/{org}/summary`. Composes the same top-level shape as production: `{ org_number, entity_type, data_tier, obligations[], deadlines[], upgrade_path }`. Production accepts `?from_date=` and `?horizon_months=`; sandbox accepts the same params for client-shape parity but returns the fixture's static deadline list verbatim.","tags":["Sandbox"],"operationId":"sandboxCompanySummary","security":[{"BearerAuth":[]}],"parameters":[{"name":"from_date","in":"query","required":false,"schema":{"type":"string"},"description":"Accepted for production-shape parity. Production also uses `type: string` at the wire layer. Sandbox does NOT enforce the date-format constraint at runtime and returns the static fixture regardless."},{"name":"horizon_months","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":60},"description":"Schema mirrors production exactly (integer 1-60). Sandbox runtime does NOT validate the value; the static fixture is returned regardless."},{"$ref":"#/components/parameters/SandboxSimulateError"}],"responses":{"200":{"$ref":"#/components/responses/SandboxSummarySuccess"},"400":{"$ref":"#/components/responses/SandboxError"},"401":{"$ref":"#/components/responses/SandboxError"},"403":{"$ref":"#/components/responses/SandboxError"},"404":{"$ref":"#/components/responses/SandboxError"},"500":{"$ref":"#/components/responses/SandboxError"}}},"options":{"summary":"CORS preflight for the sandbox summary endpoint","description":"Returns 204 with the standard sandbox CORS headers. See sandbox context endpoint OPTIONS for the full preflight contract.","tags":["Sandbox"],"operationId":"sandboxCompanySummaryOptions","security":[],"responses":{"204":{"$ref":"#/components/responses/SandboxOptionsPreflight"}}}},"/api/v1/sandbox/company/{org}/audit":{"parameters":[{"name":"org","in":"path","required":true,"schema":{"type":"string","pattern":"^\\d{9}$"},"description":"9-digit Norwegian organisation number. See sandbox context endpoint for reserved-org documentation."},{"$ref":"#/components/parameters/CorrelationIdHeader"}],"get":{"summary":"Sandbox mirror of /api/v1/company/{org}/audit (zero-auth)","description":"Zero-auth sandbox mirror of `GET /api/v1/company/{org}/audit`. Pagination is a stub: the fixture's audit_entries array is returned in full; `pagination.has_more: false` always. The full production query-param surface (`limit`, `before`, `before_id`, `since`, `until`, `action`, `initiated_by`) is documented for shape parity so generated clients can prod→sandbox swap with the same request layer; sandbox returns the static fixture regardless of values per the deterministic-fixture contract (Rule 9).","tags":["Sandbox"],"operationId":"sandboxCompanyAudit","security":[{"BearerAuth":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"description":"Schema mirrors production exactly (integer 1-200, default 50). Sandbox runtime does NOT validate the integer-range bounds and does NOT apply the value; the static fixture's full audit_entries array is returned regardless. The `pagination.limit` echoed on the response is always the production default (50)."},{"name":"before","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"Production keyset cursor companion (ISO 8601 / RFC 3339). Schema mirrors production exactly; `format: date-time` is non-validating in OpenAPI 3.1, so generated clients see the typing without sandbox runtime enforcement. Sandbox does NOT validate or apply the value (deterministic-fixture model)."},{"name":"before_id","in":"query","required":false,"schema":{"type":"string","format":"uuid"},"description":"Production keyset cursor id companion (UUID v4). Schema mirrors production exactly; `format: uuid` is documentation-only at the OpenAPI layer, sandbox does NOT enforce or apply the value."},{"name":"since","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"Production lower-bound timestamp filter (ISO 8601). Schema mirrors production exactly; sandbox does NOT validate or apply the value."},{"name":"until","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"Production upper-bound timestamp filter. Schema mirrors production exactly; sandbox does NOT validate or apply the value."},{"name":"action","in":"query","required":false,"schema":{"$ref":"#/components/schemas/AuditAction"},"description":"Schema mirrors production exactly ($ref AuditAction enum). Generated clients see the typed enum on both prod and sandbox surfaces. Sandbox runtime does NOT validate against the enum and does NOT filter the response — the full fixture is returned regardless."},{"name":"initiated_by","in":"query","required":false,"schema":{"$ref":"#/components/schemas/AuditInitiatedBy"},"description":"Schema mirrors production exactly ($ref AuditInitiatedBy enum). Generated clients see the typed enum on both prod and sandbox surfaces. Sandbox runtime does NOT validate against the enum and does NOT filter the response."},{"$ref":"#/components/parameters/SandboxSimulateError"}],"responses":{"200":{"$ref":"#/components/responses/SandboxAuditSuccess"},"400":{"$ref":"#/components/responses/SandboxError"},"401":{"$ref":"#/components/responses/SandboxError"},"403":{"$ref":"#/components/responses/SandboxError"},"404":{"$ref":"#/components/responses/SandboxError"},"500":{"$ref":"#/components/responses/SandboxError"}}},"options":{"summary":"CORS preflight for the sandbox audit endpoint","description":"Returns 204 with the standard sandbox CORS headers. See sandbox context endpoint OPTIONS for the full preflight contract.","tags":["Sandbox"],"operationId":"sandboxCompanyAuditOptions","security":[],"responses":{"204":{"$ref":"#/components/responses/SandboxOptionsPreflight"}}}},"/api/v1/sandbox/auth/approval-token":{"post":{"summary":"Sandbox mirror of /api/v1/auth/approval-token (zero-auth)","description":"Zero-auth deterministic mint of a sandbox-scoped approval token bound to (org_number, action). The returned token is exactly 49 characters: 17-char `sandbox-approval-` prefix + 32-char lowercase-hex HMAC-SHA-256 suffix. Same input → same token, every time (Rule 9). Production approval-token validators MUST treat any token bearing this prefix as invalid in production code paths.\n\n`expires_at` is INFORMATIONAL ONLY — sandbox /actions/execute does NOT enforce it against wall-clock time (sandbox is deterministic-by-construction; expires_at is metadata for the agent's UI). Sandbox does not write to the production `approval_tokens` table — there is no DB row backing the returned token. The token is purely an HMAC-derived string.\n\nThe HMAC secret used here is `SANDBOX_RECEIPT_HMAC_KEY` (env var, REQUIRED when `VERCEL_ENV=production` — see DECISIONS.md PR-074-sandbox-execute iteration-1 fix for why VERCEL_ENV not NODE_ENV; falls back to a known-public dev literal in non-production environments). `SANDBOX_RECEIPT_HMAC_KEY` is by design fully separable from the production `RECEIPT_HMAC_SECRET` — sandbox tokens cannot be cross-verified as production receipts and vice versa. PR-074-sandbox-execute (DECISIONS.md).","tags":["Sandbox"],"operationId":"sandboxAuthApprovalToken","security":[{"BearerAuth":[]}],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","pattern":"^[ -~]{1,255}$"},"description":"Accepted for cross-client header parity with production but NOT enforced (no idempotency cache lookup, no per-key replay binding) — sandbox is deterministic by construction, so the same body always produces the same token regardless of whether an Idempotency-Key was supplied. Charset / length validated at the boundary so a malformed header still 400s before any HMAC computation. Production /v1/auth/approval-token enforces single-use binding via `idempotency_keys` table; sandbox does not write to that table."},{"$ref":"#/components/parameters/CorrelationIdHeader"},{"$ref":"#/components/parameters/SandboxSimulateError"}],"requestBody":{"required":true,"description":"UTF-8 request body hard-capped at 16384 bytes (16 KiB). Larger bodies return 413 — `readBodyAsTextWithLimit` enforces the budget DURING streaming consumption so a lying Content-Length cannot force the runtime to buffer past the limit before the cap fires.","content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["org_number","action"],"properties":{"org_number":{"type":"string","pattern":"^\\d{9}$","description":"9-digit reserved synthetic org. Must appear in the `data.sandbox.reserved_test_org_numbers` array on the response body of `GET /api/v1/capabilities` (or 999000901-904 for reserved error orgs). Any other 9-digit value returns 400 VALIDATION_FAILED — distinct from the read-side which 404s on unknown orgs (sandbox write-side invariant)."},"action":{"type":"string","enum":["mva_melding","a_melding"],"description":"Closed enum mirroring production `/api/v1/actions/execute`'s ActionTypeEnum."}}}}}},"responses":{"200":{"description":"Token minted. `data.approval_token` is the 49-char string the agent passes to /api/v1/sandbox/actions/execute via the body's `approval_token` field.","headers":{"Cache-Control":{"description":"`no-store` — sandbox responses are never cached (deterministic by construction).","schema":{"type":"string","enum":["no-store"]}},"Access-Control-Allow-Origin":{"description":"`*`.","schema":{"type":"string"}},"Access-Control-Allow-Methods":{"description":"`POST, OPTIONS` — write-side override of the read-side `GET, OPTIONS` default. Emitted by `applySandboxWriteSideCorsHeaders`.","schema":{"type":"string","enum":["POST, OPTIONS"]}},"Access-Control-Allow-Headers":{"description":"`Authorization, Content-Type, Idempotency-Key, X-Correlation-ID` — write-side safe list. PR-SCOPE-002: `Authorization` is on this list because the sandbox is auth-gated.","schema":{"type":"string"}},"Access-Control-Allow-Credentials":{"description":"`false` — mandatory on every write-side sandbox response.","schema":{"type":"string","enum":["false"]}},"Access-Control-Expose-Headers":{"description":"`X-Correlation-ID, Link, X-Sandbox, X-Apier-Upstream-Healthy` — sandbox-wide expose set; `X-Apier-Upstream-Healthy` is a no-op on this route (only execute emits it) but listed for byte-stability across the surface.","schema":{"type":"string"}},"X-Sandbox":{"description":"Always `true`.","schema":{"type":"string","enum":["true"]}},"X-Content-Type-Options":{"description":"`nosniff`.","schema":{"type":"string","enum":["nosniff"]}},"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"description":"`</openapi.json>; rel=\"service-desc\"` per Rule 4.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["success","data","_meta"],"properties":{"success":{"const":true},"data":{"type":"object","additionalProperties":false,"required":["approval_token","expires_at","scope"],"properties":{"approval_token":{"type":"string","minLength":49,"maxLength":49,"pattern":"^sandbox-approval-[a-f0-9]{32}$"},"expires_at":{"type":"string","format":"date-time","description":"5 min from SANDBOX_NOW (deterministic). Informational only; /actions/execute does NOT enforce expires_at."},"scope":{"type":"object","additionalProperties":false,"required":["org_number","action"],"properties":{"org_number":{"type":"string"},"action":{"type":"string"}}}}},"_meta":{"$ref":"#/components/schemas/SandboxMeta"}}}}}},"400":{"$ref":"#/components/responses/SandboxWriteSideError"},"401":{"$ref":"#/components/responses/SandboxWriteSideError"},"403":{"$ref":"#/components/responses/SandboxWriteSideError"},"413":{"$ref":"#/components/responses/SandboxWriteSideError","description":"UTF-8 request body exceeded 16384 bytes (16 KiB). The streaming `readBodyAsTextWithLimit` reader fires this before any JSON parsing happens, so the response body's `error_code` is `REQUEST_TOO_LARGE`."},"500":{"$ref":"#/components/responses/SandboxWriteSideError"}}},"options":{"summary":"CORS preflight for the sandbox approval-token endpoint","description":"Returns 204 with write-side CORS — `Access-Control-Allow-Methods: POST, OPTIONS`; `Access-Control-Allow-Headers: Authorization, Content-Type, Idempotency-Key, X-Correlation-ID`; `Access-Control-Allow-Credentials: false` (Bearer tokens flow cross-origin with credentials=false). PR-SCOPE-002: `Authorization` is on Allow-Headers because the sandbox is auth-gated. Max-Age 86400.","tags":["Sandbox"],"operationId":"sandboxAuthApprovalTokenOptions","security":[],"responses":{"204":{"$ref":"#/components/responses/SandboxWriteSideOptionsPreflight"}}}},"/api/v1/sandbox/actions/plan":{"post":{"summary":"Sandbox plan describing the prerequisite chain (zero-auth)","description":"Returns the deterministic prerequisite chain for executing a regulatory action. Plan describes 5 steps in order: verify_company → verify_delegation → validate_payload → mint_approval_token → execute. Sandbox plan is its own contract — there is NO production `/api/v1/actions/plan` endpoint to mirror today. PR-074-sandbox-execute introduced this surface so PR-078 GP3's full chain can run end-to-end on sandbox.\n\nPlan does NOT emit the approval token directly — call `/api/v1/sandbox/auth/approval-token` after plan. The plan response includes `mint_endpoint` (the URL to call next) and `requires_approval` (boolean). These field names deliberately AVOID the substring `token` because the PR-078 integration suite's secret-scanner walks every response key against `/token|secret|password|key_hash|api_key/i` — using `requires_approval_token` / `approval_token_endpoint` would have triggered false positives on a deterministic metadata field.","tags":["Sandbox"],"operationId":"sandboxActionsPlan","security":[{"BearerAuth":[]}],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","pattern":"^[ -~]{1,255}$"},"description":"Accepted for cross-client header parity with production but NOT enforced — sandbox plan is a deterministic shape; the same body produces the same plan on every call. Charset / length validated at the boundary."},{"$ref":"#/components/parameters/CorrelationIdHeader"},{"$ref":"#/components/parameters/SandboxSimulateError"}],"requestBody":{"required":true,"description":"UTF-8 request body hard-capped at 16384 bytes (16 KiB). Larger bodies return 413 — `readBodyAsTextWithLimit` enforces the budget DURING streaming consumption so a lying Content-Length cannot force the runtime to buffer past the limit before the cap fires.","content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["org_number","action"],"properties":{"org_number":{"type":"string","pattern":"^\\d{9}$","description":"9-digit reserved synthetic org. Must appear in the `data.sandbox.reserved_test_org_numbers` array on the response body of `GET /api/v1/capabilities` (or 999000901-904 for reserved error orgs). Any other 9-digit value returns 400 VALIDATION_FAILED — distinct from the read-side which 404s on unknown orgs (sandbox write-side invariant)."},"action":{"type":"string","enum":["mva_melding","a_melding"]}}}}}},"responses":{"200":{"description":"Plan returned. Each step has `step` (machine-readable id), `status` (`ok` for prerequisites already satisfied, `pending` for what the agent must do next), and `description_nb` (Norwegian bokmål prose).","headers":{"Cache-Control":{"description":"`no-store` — sandbox responses are never cached (deterministic by construction).","schema":{"type":"string","enum":["no-store"]}},"Access-Control-Allow-Origin":{"description":"`*`.","schema":{"type":"string"}},"Access-Control-Allow-Methods":{"description":"`POST, OPTIONS` — write-side override of the read-side `GET, OPTIONS` default. Emitted by `applySandboxWriteSideCorsHeaders`.","schema":{"type":"string","enum":["POST, OPTIONS"]}},"Access-Control-Allow-Headers":{"description":"`Authorization, Content-Type, Idempotency-Key, X-Correlation-ID` — write-side safe list. PR-SCOPE-002: `Authorization` is on this list because the sandbox is auth-gated.","schema":{"type":"string"}},"Access-Control-Allow-Credentials":{"description":"`false` — mandatory on every write-side sandbox response.","schema":{"type":"string","enum":["false"]}},"Access-Control-Expose-Headers":{"description":"`X-Correlation-ID, Link, X-Sandbox, X-Apier-Upstream-Healthy` — sandbox-wide expose set; `X-Apier-Upstream-Healthy` is a no-op on this route (only execute emits it) but listed for byte-stability across the surface.","schema":{"type":"string"}},"X-Sandbox":{"description":"Always `true`.","schema":{"type":"string","enum":["true"]}},"X-Content-Type-Options":{"description":"`nosniff`.","schema":{"type":"string","enum":["nosniff"]}},"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"description":"`</openapi.json>; rel=\"service-desc\"` per Rule 4.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["success","data","_meta"],"properties":{"success":{"const":true},"data":{"type":"object","additionalProperties":false,"required":["org_number","action","plan","requires_approval","mint_endpoint"],"properties":{"org_number":{"type":"string"},"action":{"type":"string"},"plan":{"type":"array","description":"Five-element tuple, fixed order: verify_company → verify_delegation → validate_payload → mint_approval_token → execute. Iteration-24 polish (CodeRabbit Major): pinned to a JSON Schema 2020-12 `prefixItems` tuple with `minItems: 5` + `maxItems: 5` + per-step `step: { const: ... }` so the published contract matches the runtime invariant. SDK consumers can switch on positional index; type generators emit a tuple type.","minItems":5,"maxItems":5,"prefixItems":[{"type":"object","required":["step","status","description_nb"],"additionalProperties":false,"properties":{"step":{"type":"string","const":"verify_company"},"status":{"type":"string","enum":["ok","pending"]},"description_nb":{"type":"string"}}},{"type":"object","required":["step","status","description_nb"],"additionalProperties":false,"properties":{"step":{"type":"string","const":"verify_delegation"},"status":{"type":"string","enum":["ok","pending"]},"description_nb":{"type":"string"}}},{"type":"object","required":["step","status","description_nb"],"additionalProperties":false,"properties":{"step":{"type":"string","const":"validate_payload"},"status":{"type":"string","enum":["ok","pending"]},"description_nb":{"type":"string"}}},{"type":"object","required":["step","status","description_nb"],"additionalProperties":false,"properties":{"step":{"type":"string","const":"mint_approval_token"},"status":{"type":"string","enum":["ok","pending"]},"description_nb":{"type":"string"}}},{"type":"object","required":["step","status","description_nb"],"additionalProperties":false,"properties":{"step":{"type":"string","const":"execute"},"status":{"type":"string","enum":["ok","pending"]},"description_nb":{"type":"string"}}}],"items":{"type":"object","$comment":"Permissive `items` schema — required by Spectral oas3 array-items rule. Closure of the tuple is enforced via `minItems: 5` + `maxItems: 5` + the five-element `prefixItems` above; no additional items can exist past index 4."}},"requires_approval":{"type":"boolean","description":"Always true today — every action sandbox supports requires an approval token before live execute. Field name deliberately omits `token` substring (secret-scanner avoidance)."},"mint_endpoint":{"type":"string","description":"URL of the approval-token mint endpoint (`/api/v1/sandbox/auth/approval-token`). Field name deliberately omits `token` substring (secret-scanner avoidance)."}}},"_meta":{"$ref":"#/components/schemas/SandboxMeta"}}}}}},"400":{"$ref":"#/components/responses/SandboxWriteSideError"},"401":{"$ref":"#/components/responses/SandboxWriteSideError"},"403":{"$ref":"#/components/responses/SandboxWriteSideError"},"413":{"$ref":"#/components/responses/SandboxWriteSideError","description":"UTF-8 request body exceeded 16384 bytes (16 KiB). The streaming `readBodyAsTextWithLimit` reader fires this before any JSON parsing happens, so the response body's `error_code` is `REQUEST_TOO_LARGE`."},"500":{"$ref":"#/components/responses/SandboxWriteSideError"}}},"options":{"summary":"CORS preflight for the sandbox plan endpoint","description":"Returns 204 with write-side CORS (POST/OPTIONS, `Authorization` on Allow-Headers per PR-SCOPE-002, Allow-Credentials: false).","tags":["Sandbox"],"operationId":"sandboxActionsPlanOptions","security":[],"responses":{"204":{"$ref":"#/components/responses/SandboxWriteSideOptionsPreflight"}}}},"/api/v1/sandbox/actions/execute":{"post":{"summary":"Sandbox mirror of /api/v1/actions/execute (zero-auth, dry-run + live mock)","description":"The sandbox write-side execute endpoint. Two paths discriminated by `?dry_run=`:\n\n**Dry-run (`?dry_run=true`)**: returns a deterministic `validation` block describing the five PR-073 checks, with `data.dry_run: true` and NO `data.altinn_receipt_id`. PR-A reliability envelope (`execution_guarantee` + `outcome` at top level) emitted on every response (universal-emission contract). No approval token required.\n\n**Live mock (no `dry_run`, or `dry_run=false`)**: requires `approval_token` in the body. Runs the full validation gauntlet — presence → prefix → length → hex-only → re-derive → `crypto.timingSafeEqual`. Returns a deterministic receipt envelope with `altinn_receipt_id` (HMAC-derived 32-char hex), `audit_log_id` (UUID v5 from sandbox namespace), `signature` (HMAC over canonical receipt message), and `signature_algorithm: 'HMAC-SHA256-v1'`. Top-level + nested `_sandbox_marker` field flags every sandbox receipt as synthetic.\n\nSandbox does NOT enforce `expires_at` on the approval token — sandbox is deterministic-by-construction; SANDBOX_NOW is fixed (2026-05-04T12:00:00+02:00). No DB writes to audit_log / provenance_log / receipts / approval_tokens.\n\n`?simulate_error=` precedes everything except validation; reserved error orgs (999000901-904) fire BEFORE token validation so an agent can probe the explainer envelope without a valid token.\n\n**Idempotency-Key is required (Amendment 61 §5.4 / PR-IDEMPOTENCY-AUDIT).** Mirrors the production write surface so SDK retry code paths work against either endpoint without branching. A missing header returns 400 VALIDATION_FAILED with `details.field: 'Idempotency-Key'`. Sandbox is deterministic-by-construction (same body produces the same receipt on every call) so replays return verbatim — but unlike production, sandbox does NOT write a `was_idempotent_replay=true` audit_log row on replay (sandbox writes ZERO DB rows per DECISIONS.md PR-074 § 2; the idempotency-replay audit row is a production-only forensic artifact).","tags":["Sandbox"],"operationId":"sandboxActionsExecute","security":[{"BearerAuth":[]}],"parameters":[{"name":"dry_run","in":"query","required":false,"schema":{"type":"string","enum":["true","false"]},"description":"Strict z.enum — `'1'` / `'yes'` / `''` / garbage all return 400 (Rule 26 — empty value rejected by iteration-2 polish, no silent fallthrough)."},{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","pattern":"^[ -~]{1,255}$"},"example":"<unique-client-generated-string>","description":"REQUIRED on /api/v1/sandbox/actions/execute (Amendment 61 §5.4 / PR-IDEMPOTENCY-AUDIT). The HEADER is required and validated at the boundary (1-255 printable ASCII; missing → 400 VALIDATION_FAILED with `details.field: 'Idempotency-Key'`). What sandbox does NOT do — distinct from production — is enforce single-use REPLAY BINDING via the `idempotency_keys` table: on production the same key + same body within 24h returns the cached response and emits `Idempotent-Replay: true`, while sandbox computes the response fresh every call (deterministic-by-construction; same body always returns the same response, so no cache is needed). The contract MATCHES production at the header level so SDK retry code paths work against either surface. PR-074-sandbox-execute § Sandbox Middleware Exemptions still applies: sandbox writes ZERO `audit_log` rows on replay — the `was_idempotent_replay=true` forensic row is a production-only artifact."},{"$ref":"#/components/parameters/CorrelationIdHeader"},{"$ref":"#/components/parameters/SandboxSimulateError"}],"requestBody":{"required":true,"description":"UTF-8 request body hard-capped at 16384 bytes (16 KiB). Larger bodies return 413 — `readBodyAsTextWithLimit` enforces the budget DURING streaming consumption so a lying Content-Length cannot force the runtime to buffer past the limit before the cap fires.","content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["org_number","action","payload"],"properties":{"org_number":{"type":"string","pattern":"^\\d{9}$","description":"9-digit reserved synthetic org. Must appear in the `data.sandbox.reserved_test_org_numbers` array on the response body of `GET /api/v1/capabilities` (or 999000901-904 for reserved error orgs). Any other 9-digit value returns 400 VALIDATION_FAILED — distinct from the read-side which 404s on unknown orgs (sandbox write-side invariant)."},"action":{"type":"string","enum":["mva_melding","a_melding"]},"payload":{"type":"object","additionalProperties":true,"description":"Free-shape upstream payload. Sandbox does NOT validate per-action shape — fixture data is the only happy-path observation. Production `/api/v1/actions/execute` applies per-action discriminated Zod."},"approval_token":{"type":"string","pattern":"^[ -~]{1,255}$","description":"REQUIRED on live mode (no dry_run, or dry_run=false). OPTIONAL on dry_run. The schema pattern is DELIBERATELY permissive at the contract boundary (printable ASCII, 1-255 chars) and NOT the canonical sandbox-prefixed shape — the route validation gauntlet (presence then prefix then length then hex-only then re-derive then constant-time compare) is what enforces the canonical format and emits structured 401 AUTH_EXPIRED_TOKEN responses for malformed tokens. Tightening the schema to the canonical regex would shift those rejections to 400 VALIDATION_FAILED, losing the AUTH error class the integration suite (PR-078 GP3) relies on. The route comment at src/app/api/v1/sandbox/actions/execute/route.ts (Step-4 boundary Zod) records the same rationale. Iteration-19 polish (CodeRabbit Minor): description tightened to make the boundary-vs-gauntlet split visible to SDK generators / contract readers."}}}}}},"responses":{"200":{"description":"Success. Body shape on dry_run: `{ success: true, data: { dry_run: true, org_number, action, validation: {...} }, execution_guarantee, outcome, _meta }`. Body shape on live: `{ success: true, data: { dry_run: false, receipt: {...} }, execution_guarantee, outcome, _sandbox_marker, _meta }`. `outcome.is_final: true`, `outcome.requires_followup: false`, `outcome.success_probability: 0.99`, `outcome.confidence: 'high'` on the happy path. Iteration-31 polish (CodeRabbit Major): description aligned with the schema + runtime — dry-run `data` carries `org_number` + `action` echo fields too (the route stamps them so SDK clients can correlate the response to the request without re-reading the body).","headers":{"Cache-Control":{"description":"`no-store` — sandbox responses are never cached (deterministic by construction).","schema":{"type":"string","enum":["no-store"]}},"Access-Control-Allow-Origin":{"description":"`*`.","schema":{"type":"string"}},"Access-Control-Allow-Methods":{"description":"`POST, OPTIONS` — write-side override of the read-side `GET, OPTIONS` default. Emitted by `applySandboxWriteSideCorsHeaders`.","schema":{"type":"string","enum":["POST, OPTIONS"]}},"Access-Control-Allow-Headers":{"description":"`Authorization, Content-Type, Idempotency-Key, X-Correlation-ID` — write-side safe list. PR-SCOPE-002: `Authorization` is on this list because the sandbox is auth-gated. `approval_token` is in the BODY, not a header, so `X-Approval-Token` is also not advertised.","schema":{"type":"string"}},"Access-Control-Allow-Credentials":{"description":"`false` — mandatory on every write-side sandbox response.","schema":{"type":"string","enum":["false"]}},"Access-Control-Expose-Headers":{"description":"`X-Correlation-ID, Link, X-Sandbox, X-Apier-Upstream-Healthy` — `X-Apier-Upstream-Healthy` is the PR-A reliability header this route emits on every response.","schema":{"type":"string"}},"X-Sandbox":{"description":"Always `true`.","schema":{"type":"string","enum":["true"]}},"X-Apier-Upstream-Healthy":{"description":"`true` or `false` — PR-A reliability signal. Set on every action response (success or error) so SDK parsers don't have to branch on shape.","schema":{"type":"string","enum":["true","false"]}},"X-Content-Type-Options":{"description":"`nosniff`.","schema":{"type":"string","enum":["nosniff"]}},"X-Correlation-ID":{"$ref":"#/components/headers/CorrelationIdResponseHeader"},"Link":{"description":"`</openapi.json>; rel=\"service-desc\"` per Rule 4.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"description":"Discriminated by `data.dry_run` boolean. Iteration-24 polish (CodeRabbit Major): split the success envelope into two `oneOf` branches so `_sandbox_marker` is REQUIRED on the live receipt branch and ABSENT on the dry-run branch — matches the runtime invariant (the route only stamps `_sandbox_marker` on the live path). Previously the marker was an optional sibling on a single shared schema, so a marker-less live receipt would have validated; now SDK consumers can enforce the sandbox-safety invariant from the spec.","oneOf":[{"type":"object","additionalProperties":false,"required":["success","data","execution_guarantee","outcome","_meta"],"properties":{"success":{"const":true},"data":{"$ref":"#/components/schemas/SandboxExecuteDryRunData"},"execution_guarantee":{"$ref":"#/components/schemas/ExecutionGuarantee"},"outcome":{"$ref":"#/components/schemas/Outcome"},"_meta":{"$ref":"#/components/schemas/SandboxMeta"}}},{"type":"object","additionalProperties":false,"required":["success","data","execution_guarantee","outcome","_sandbox_marker","_meta"],"properties":{"success":{"const":true},"data":{"$ref":"#/components/schemas/SandboxExecuteLiveData"},"execution_guarantee":{"$ref":"#/components/schemas/ExecutionGuarantee"},"outcome":{"$ref":"#/components/schemas/Outcome"},"_sandbox_marker":{"type":"string","const":"SANDBOX_NOT_REAL_GOV_RESPONSE","description":"Top-level marker on the receipt envelope. REQUIRED on the live branch — government_response_raw inside the receipt also carries the same marker (defense in depth)."},"_meta":{"$ref":"#/components/schemas/SandboxMeta"}}}]}}}},"400":{"$ref":"#/components/responses/SandboxExecuteError"},"401":{"$ref":"#/components/responses/SandboxExecuteError"},"403":{"$ref":"#/components/responses/SandboxExecuteError"},"413":{"$ref":"#/components/responses/SandboxExecuteError","description":"UTF-8 request body exceeded 16384 bytes (16 KiB). The streaming `readBodyAsTextWithLimit` reader fires this before any JSON parsing happens, so the response body's `error_code` is `REQUEST_TOO_LARGE`."},"500":{"$ref":"#/components/responses/SandboxExecuteError"}}},"options":{"summary":"CORS preflight for the sandbox execute endpoint","description":"Returns 204 with write-side CORS (POST/OPTIONS, `Authorization` on Allow-Headers per PR-SCOPE-002, Allow-Credentials: false). `approval_token` is in the body, NOT a header — so X-Approval-Token is NOT advertised in Allow-Headers.","tags":["Sandbox"],"operationId":"sandboxActionsExecuteOptions","security":[],"responses":{"204":{"$ref":"#/components/responses/SandboxWriteSideOptionsPreflight"}}}},"/api/v1/sandbox/public/company/{org}/context":{"parameters":[{"name":"org","in":"path","required":true,"schema":{"type":"string","pattern":"^999999999$"},"description":"Public sandbox accepts ONLY the literal `999999999` fixture org. Any other 9-digit value returns 400 PUBLIC_SANDBOX_ORG_NOT_PERMITTED with the rejected input NEVER echoed in the body (Rule 24). The internal /api/v1/sandbox/* surface accepts the wider 999000001-005 fixture set; use that surface when failure-flow / `?simulate_error=` testing is needed."},{"$ref":"#/components/parameters/CorrelationIdHeader"}],"get":{"summary":"Public sandbox mirror of /api/v1/company/{org}/context (zero-auth, IP rate-limited)","description":"Unauthenticated. Per-IP rate limit 100 requests / hour on the dedicated 'public-sandbox' bucket. Org 999999999 only. Response shape is byte-equivalent to the internal `/api/v1/sandbox/company/{org}/context` mirror (reuses the same SandboxContextEnvelope schema). Two header differences from the internal surface: `Access-Control-Allow-Methods: GET, POST, OPTIONS` (unified across the public-sandbox routes); `X-Robots-Tag: noindex` and `X-Apier-Sandbox: public` are added so SDK clients can detect the surface without parsing the URL. PROVENANCE EXEMPT — no `_meta.response_hash`, no `provenance_log` row (CLAUDE.md Rule 31 sandbox carve-out).","tags":["Sandbox"],"operationId":"publicSandboxCompanyContext","security":[],"responses":{"200":{"$ref":"#/components/responses/SandboxContextSuccess"},"400":{"$ref":"#/components/responses/PublicSandboxError","description":"PUBLIC_SANDBOX_ORG_NOT_PERMITTED when the path org is not 999999999."},"429":{"$ref":"#/components/responses/PublicSandboxError","description":"RATE_LIMIT_EXCEEDED when the IP bucket is full. Carries Retry-After header."},"500":{"$ref":"#/components/responses/PublicSandboxError"}}},"options":{"summary":"CORS preflight for the public-sandbox context endpoint","description":"204 with `Access-Control-Allow-Methods: GET, POST, OPTIONS` (unified across the public-sandbox surface). No credentials.","tags":["Sandbox"],"operationId":"publicSandboxCompanyContextOptions","security":[],"responses":{"204":{"$ref":"#/components/responses/PublicSandboxOptionsPreflight"}}}},"/api/v1/sandbox/public/company/{org}/obligations":{"parameters":[{"name":"org","in":"path","required":true,"schema":{"type":"string","pattern":"^999999999$"},"description":"See public-sandbox context endpoint for the org-only-999999999 contract."},{"$ref":"#/components/parameters/CorrelationIdHeader"}],"get":{"summary":"Public sandbox mirror of /api/v1/company/{org}/obligations (zero-auth, IP rate-limited)","description":"Unauthenticated. Per-IP rate limit 100 requests / hour. Org 999999999 only. Response shape mirrors SandboxObligationsEnvelope. PROVENANCE EXEMPT.","tags":["Sandbox"],"operationId":"publicSandboxCompanyObligations","security":[],"responses":{"200":{"$ref":"#/components/responses/SandboxObligationsSuccess"},"400":{"$ref":"#/components/responses/PublicSandboxError","description":"PUBLIC_SANDBOX_ORG_NOT_PERMITTED when the path org is not 999999999."},"429":{"$ref":"#/components/responses/PublicSandboxError"},"500":{"$ref":"#/components/responses/PublicSandboxError"}}},"options":{"summary":"CORS preflight for the public-sandbox obligations endpoint","description":"204 with `Access-Control-Allow-Methods: GET, POST, OPTIONS` and the public-sandbox header set.","tags":["Sandbox"],"operationId":"publicSandboxCompanyObligationsOptions","security":[],"responses":{"204":{"$ref":"#/components/responses/PublicSandboxOptionsPreflight"}}}},"/api/v1/sandbox/public/company/{org}/deadlines":{"parameters":[{"name":"org","in":"path","required":true,"schema":{"type":"string","pattern":"^999999999$"},"description":"See public-sandbox context endpoint for the org-only-999999999 contract."},{"$ref":"#/components/parameters/CorrelationIdHeader"}],"get":{"summary":"Public sandbox mirror of /api/v1/company/{org}/deadlines (zero-auth, IP rate-limited)","description":"Unauthenticated. Per-IP rate limit 100 requests / hour. Org 999999999 only. Response shape mirrors SandboxDeadlinesEnvelope. PROVENANCE EXEMPT.","tags":["Sandbox"],"operationId":"publicSandboxCompanyDeadlines","security":[],"parameters":[{"name":"from_date","in":"query","required":false,"schema":{"type":"string"},"description":"Accepted for parity with the internal sandbox surface; sandbox does NOT filter on this value (deterministic-fixture model)."},{"name":"horizon_months","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":60},"description":"Accepted for parity; sandbox does NOT filter."}],"responses":{"200":{"$ref":"#/components/responses/SandboxDeadlinesSuccess"},"400":{"$ref":"#/components/responses/PublicSandboxError","description":"PUBLIC_SANDBOX_ORG_NOT_PERMITTED when the path org is not 999999999."},"429":{"$ref":"#/components/responses/PublicSandboxError"},"500":{"$ref":"#/components/responses/PublicSandboxError"}}},"options":{"summary":"CORS preflight for the public-sandbox deadlines endpoint","description":"204 with the public-sandbox CORS posture.","tags":["Sandbox"],"operationId":"publicSandboxCompanyDeadlinesOptions","security":[],"responses":{"204":{"$ref":"#/components/responses/PublicSandboxOptionsPreflight"}}}},"/api/v1/sandbox/public/company/{org}/summary":{"parameters":[{"name":"org","in":"path","required":true,"schema":{"type":"string","pattern":"^999999999$"},"description":"See public-sandbox context endpoint for the org-only-999999999 contract."},{"$ref":"#/components/parameters/CorrelationIdHeader"}],"get":{"summary":"Public sandbox mirror of /api/v1/company/{org}/summary (zero-auth, IP rate-limited)","description":"Unauthenticated. Per-IP rate limit 100 requests / hour. Org 999999999 only. Response shape mirrors SandboxSummaryEnvelope. PROVENANCE EXEMPT.","tags":["Sandbox"],"operationId":"publicSandboxCompanySummary","security":[],"parameters":[{"name":"from_date","in":"query","required":false,"schema":{"type":"string"},"description":"Accepted for parity; sandbox does NOT filter."},{"name":"horizon_months","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":60},"description":"Accepted for parity; sandbox does NOT filter."}],"responses":{"200":{"$ref":"#/components/responses/SandboxSummarySuccess"},"400":{"$ref":"#/components/responses/PublicSandboxError","description":"PUBLIC_SANDBOX_ORG_NOT_PERMITTED when the path org is not 999999999."},"429":{"$ref":"#/components/responses/PublicSandboxError"},"500":{"$ref":"#/components/responses/PublicSandboxError"}}},"options":{"summary":"CORS preflight for the public-sandbox summary endpoint","description":"204 with the public-sandbox CORS posture.","tags":["Sandbox"],"operationId":"publicSandboxCompanySummaryOptions","security":[],"responses":{"204":{"$ref":"#/components/responses/PublicSandboxOptionsPreflight"}}}},"/api/v1/sandbox/public/company/{org}/audit":{"parameters":[{"name":"org","in":"path","required":true,"schema":{"type":"string","pattern":"^999999999$"},"description":"See public-sandbox context endpoint for the org-only-999999999 contract."},{"$ref":"#/components/parameters/CorrelationIdHeader"}],"get":{"summary":"Public sandbox mirror of /api/v1/company/{org}/audit (zero-auth, IP rate-limited)","description":"Unauthenticated. Per-IP rate limit 100 requests / hour. Org 999999999 only. Response shape mirrors SandboxAuditEnvelope (double-wrapped data nesting preserved). PROVENANCE EXEMPT.","tags":["Sandbox"],"operationId":"publicSandboxCompanyAudit","security":[],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"description":"Accepted for parity; sandbox does NOT enforce."},{"name":"before","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"Accepted for parity."},{"name":"before_id","in":"query","required":false,"schema":{"type":"string","format":"uuid"},"description":"Accepted for parity."},{"name":"since","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"Accepted for parity."},{"name":"until","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"Accepted for parity."},{"name":"action","in":"query","required":false,"schema":{"$ref":"#/components/schemas/AuditAction"},"description":"Accepted for parity."},{"name":"initiated_by","in":"query","required":false,"schema":{"$ref":"#/components/schemas/AuditInitiatedBy"},"description":"Accepted for parity."}],"responses":{"200":{"$ref":"#/components/responses/SandboxAuditSuccess"},"400":{"$ref":"#/components/responses/PublicSandboxError","description":"PUBLIC_SANDBOX_ORG_NOT_PERMITTED when the path org is not 999999999."},"429":{"$ref":"#/components/responses/PublicSandboxError"},"500":{"$ref":"#/components/responses/PublicSandboxError"}}},"options":{"summary":"CORS preflight for the public-sandbox audit endpoint","description":"204 with the public-sandbox CORS posture.","tags":["Sandbox"],"operationId":"publicSandboxCompanyAuditOptions","security":[],"responses":{"204":{"$ref":"#/components/responses/PublicSandboxOptionsPreflight"}}}},"/api/v1/sandbox/public/auth/approval-token":{"post":{"summary":"Public sandbox approval-token mint (zero-auth, IP rate-limited)","description":"Unauthenticated. Per-IP rate limit 100 requests / hour. POST body capped at 32768 bytes (32 KiB) — larger bodies return 413 PUBLIC_SANDBOX_BODY_TOO_LARGE; the streaming `readBodyAsTextWithLimit` enforces the cap during consumption (lying-Content-Length attackers get aborted at the byte budget, never buffered to memory). Org 999999999 only. Same HMAC derivation as the internal sandbox approval-token endpoint — same input → same 49-char `sandbox-approval-{32 hex}` token. PROVENANCE EXEMPT.","tags":["Sandbox"],"operationId":"publicSandboxAuthApprovalToken","security":[],"parameters":[{"$ref":"#/components/parameters/CorrelationIdHeader"}],"requestBody":{"required":true,"description":"UTF-8 body capped at 32768 bytes.","content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["org_number","action"],"properties":{"org_number":{"type":"string","pattern":"^\\d{9}$","description":"9-digit Norwegian organisation number. The runtime accepts any value matching this regex at the SCHEMA layer; non-999999999 values still 400 with PUBLIC_SANDBOX_ORG_NOT_PERMITTED at the runtime org-gate. Documenting the broader regex keeps the 400 branch reachable for generated clients."},"action":{"type":"string","enum":["mva_melding","a_melding"]}}}}}},"responses":{"200":{"description":"Token minted. Same envelope shape as the internal /api/v1/sandbox/auth/approval-token success response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["success","data","_meta"],"properties":{"success":{"const":true},"data":{"type":"object","additionalProperties":false,"required":["approval_token","expires_at","scope"],"properties":{"approval_token":{"type":"string","minLength":49,"maxLength":49,"pattern":"^sandbox-approval-[a-f0-9]{32}$"},"expires_at":{"type":"string","format":"date-time"},"scope":{"type":"object","additionalProperties":false,"required":["org_number","action"],"properties":{"org_number":{"type":"string"},"action":{"type":"string"}}}}},"_meta":{"$ref":"#/components/schemas/SandboxMeta"}}}}}},"400":{"$ref":"#/components/responses/PublicSandboxError","description":"PUBLIC_SANDBOX_ORG_NOT_PERMITTED, VALIDATION_FAILED, or body-shape error."},"413":{"$ref":"#/components/responses/PublicSandboxError","description":"PUBLIC_SANDBOX_BODY_TOO_LARGE — body exceeded 32 KiB."},"429":{"$ref":"#/components/responses/PublicSandboxError","description":"RATE_LIMIT_EXCEEDED — IP bucket full."},"500":{"$ref":"#/components/responses/PublicSandboxError"}}},"options":{"summary":"CORS preflight for the public-sandbox approval-token endpoint","description":"204 with the public-sandbox CORS posture (no credentials).","tags":["Sandbox"],"operationId":"publicSandboxAuthApprovalTokenOptions","security":[],"responses":{"204":{"$ref":"#/components/responses/PublicSandboxOptionsPreflight"}}}},"/api/v1/sandbox/public/actions/plan":{"post":{"summary":"Public sandbox plan (zero-auth, IP rate-limited)","description":"Unauthenticated. Per-IP rate limit 100 requests / hour. POST body capped at 32 KiB. Org 999999999 only. Returns the deterministic 5-step plan; mint_endpoint points at the public-sandbox approval-token URL. PROVENANCE EXEMPT.","tags":["Sandbox"],"operationId":"publicSandboxActionsPlan","security":[],"parameters":[{"$ref":"#/components/parameters/CorrelationIdHeader"}],"requestBody":{"required":true,"description":"UTF-8 body capped at 32768 bytes.","content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["org_number","action"],"properties":{"org_number":{"type":"string","pattern":"^\\d{9}$"},"action":{"type":"string","enum":["mva_melding","a_melding"]}}}}}},"responses":{"200":{"description":"Plan returned. Same shape as /api/v1/sandbox/actions/plan with mint_endpoint pointing at the public-sandbox URL.","content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["success","data","_meta"],"properties":{"success":{"const":true},"data":{"type":"object","additionalProperties":false,"required":["org_number","action","plan","requires_approval","mint_endpoint"],"properties":{"org_number":{"type":"string"},"action":{"type":"string"},"plan":{"type":"array","description":"Five-element tuple in fixed order: verify_company → verify_delegation → validate_payload → mint_approval_token → execute. Round 3 fix (CodeRabbit Major) — restored to a typed `prefixItems` tuple matching the internal /api/v1/sandbox/actions/plan response so generated clients can switch on positional index and rely on per-step `step` / `status` / `description_nb` fields.","minItems":5,"maxItems":5,"prefixItems":[{"type":"object","required":["step","status","description_nb"],"additionalProperties":false,"properties":{"step":{"type":"string","const":"verify_company"},"status":{"type":"string","enum":["ok","pending"]},"description_nb":{"type":"string"}}},{"type":"object","required":["step","status","description_nb"],"additionalProperties":false,"properties":{"step":{"type":"string","const":"verify_delegation"},"status":{"type":"string","enum":["ok","pending"]},"description_nb":{"type":"string"}}},{"type":"object","required":["step","status","description_nb"],"additionalProperties":false,"properties":{"step":{"type":"string","const":"validate_payload"},"status":{"type":"string","enum":["ok","pending"]},"description_nb":{"type":"string"}}},{"type":"object","required":["step","status","description_nb"],"additionalProperties":false,"properties":{"step":{"type":"string","const":"mint_approval_token"},"status":{"type":"string","enum":["ok","pending"]},"description_nb":{"type":"string"}}},{"type":"object","required":["step","status","description_nb"],"additionalProperties":false,"properties":{"step":{"type":"string","const":"execute"},"status":{"type":"string","enum":["ok","pending"]},"description_nb":{"type":"string"}}}],"items":{"type":"object","$comment":"Permissive `items` schema — required by Spectral oas3 array-items rule. Closure of the tuple is enforced via `minItems: 5` + `maxItems: 5` + the five-element `prefixItems` above; no additional items can exist past index 4."}},"requires_approval":{"type":"boolean"},"mint_endpoint":{"type":"string"}}},"_meta":{"$ref":"#/components/schemas/SandboxMeta"}}}}}},"400":{"$ref":"#/components/responses/PublicSandboxError"},"413":{"$ref":"#/components/responses/PublicSandboxError","description":"PUBLIC_SANDBOX_BODY_TOO_LARGE — body exceeded 32 KiB."},"429":{"$ref":"#/components/responses/PublicSandboxError"},"500":{"$ref":"#/components/responses/PublicSandboxError"}}},"options":{"summary":"CORS preflight for the public-sandbox plan endpoint","description":"204 with the public-sandbox CORS posture.","tags":["Sandbox"],"operationId":"publicSandboxActionsPlanOptions","security":[],"responses":{"204":{"$ref":"#/components/responses/PublicSandboxOptionsPreflight"}}}},"/api/v1/sandbox/public/actions/execute":{"post":{"summary":"Public sandbox execute (zero-auth, IP rate-limited, dry-run + live mock)","description":"Unauthenticated. Per-IP rate limit 100 requests / hour. POST body capped at 32 KiB. Org 999999999 only. Two paths discriminated by `?dry_run=`: dry-run returns a deterministic validation block; live runs the same approval-token gauntlet as the internal sandbox execute (presence → prefix → length → hex-only → re-derive → constant-time compare) and returns a deterministic receipt envelope. `Idempotency-Key` header is REQUIRED on every call (matches the internal /v1/sandbox/actions/execute + production write-side contract per CLAUDE.md Rule 5); a missing header returns 400 VALIDATION_FAILED with `details.field='Idempotency-Key'`. Sandbox is deterministic-by-construction so the key is validated for charset / length but NOT bound to a single-use cache. PROVENANCE EXEMPT. Unlike the internal sandbox execute, this surface does NOT emit the PR-A `execution_guarantee` + `outcome` reliability envelope at the top level — public sandbox is shape-discovery, not reliability drill.","tags":["Sandbox"],"operationId":"publicSandboxActionsExecute","security":[],"parameters":[{"name":"dry_run","in":"query","required":false,"schema":{"type":"string","enum":["true","false"]},"description":"Strict — empty / unknown values return 400."},{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","pattern":"^[\\x20-\\x7E]{1,255}$"},"example":"<unique-client-generated-string>","description":"REQUIRED on /api/v1/sandbox/public/actions/execute (CLAUDE.md Rule 5; matches the internal /v1/sandbox/actions/execute contract). Validated for charset (printable ASCII) + length (1-255) at the boundary — missing or malformed returns 400 VALIDATION_FAILED with `details.field='Idempotency-Key'`. Sandbox is deterministic-by-construction so the key is NOT bound against a single-use cache; SDK retry code paths that thread the key through both surfaces work identically."},{"$ref":"#/components/parameters/CorrelationIdHeader"}],"requestBody":{"required":true,"description":"UTF-8 body capped at 32768 bytes. `approval_token` is REQUIRED on live mode (no `dry_run` or `dry_run=false`); OPTIONAL on dry-run.","content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["org_number","action","payload"],"properties":{"org_number":{"type":"string","pattern":"^\\d{9}$"},"action":{"type":"string","enum":["mva_melding","a_melding"]},"payload":{"type":"object","additionalProperties":true},"approval_token":{"type":"string","pattern":"^[ -~]{1,255}$","description":"Boundary regex is permissive (1-255 printable ASCII); the live-mode gauntlet enforces the canonical sandbox-prefixed shape after."}}}}}},"responses":{"200":{"description":"Success. Discriminated by `data.dry_run` boolean. Dry-run: `{ success: true, data: { dry_run: true, org_number, action, validation }, _meta }`. Live: `{ success: true, data: { dry_run: false, receipt, _sandbox_marker }, _meta }` — `_sandbox_marker` lives inside `data` (paired with the nested `data.receipt.government_response_raw._sandbox_marker`) so an agent serialising any subset of `data` still sees the marker. The internal /v1/sandbox/actions/execute route additionally exposes a top-level `_sandbox_marker` sibling; this public-sandbox variant intentionally narrows to the `data`-scoped marker since the response envelope is wrapper-stable across both branches.","content":{"application/json":{"schema":{"description":"Discriminated by `data.dry_run`. Round 1 fix (CodeRabbit Major): split into a proper `oneOf` so the dry-run vs live discriminator is preserved for generated clients (the prior `data: { additionalProperties: true }` erased it), and `_sandbox_marker` sits at the response envelope level on the live branch matching the runtime emission in `src/lib/sandbox/public/post-handlers.ts`.","oneOf":[{"type":"object","additionalProperties":false,"required":["success","data","_meta"],"properties":{"success":{"const":true},"data":{"type":"object","additionalProperties":false,"required":["dry_run","org_number","action","validation"],"properties":{"dry_run":{"const":true},"org_number":{"type":"string"},"action":{"type":"string"},"validation":{"type":"object","additionalProperties":true,"description":"Deterministic 5-step PR-073 validation block — `all_passed`, `checks[]`, `disclaimer`."}}},"_meta":{"$ref":"#/components/schemas/SandboxMeta"}}},{"type":"object","additionalProperties":false,"required":["success","data","_meta"],"properties":{"success":{"const":true},"data":{"type":"object","additionalProperties":false,"required":["dry_run","receipt","_sandbox_marker"],"properties":{"dry_run":{"const":false},"receipt":{"type":"object","additionalProperties":true,"description":"Receipt envelope: `altinn_receipt_id` (HMAC-derived 32-char hex), `audit_log_id` (UUID v5 from sandbox namespace), `timestamp` (Europe/Oslo offset), `org_number`, `filing_type`, `rule_version: 'sandbox'`, `government_response_raw` (carries the nested `_sandbox_marker`), `signature` (HMAC over canonical receipt message), `signature_algorithm: 'HMAC-SHA256-v1'`."},"_sandbox_marker":{"type":"string","const":"SANDBOX_NOT_REAL_GOV_RESPONSE","description":"Nested marker inside `data` — paired with the envelope-level marker for defense in depth (partial serialisations always surface at least one)."}}},"_meta":{"$ref":"#/components/schemas/SandboxMeta"}}}]}}}},"400":{"$ref":"#/components/responses/PublicSandboxError"},"401":{"$ref":"#/components/responses/PublicSandboxError","description":"AUTH_EXPIRED_TOKEN when the live-mode gauntlet rejects the supplied approval_token."},"404":{"$ref":"#/components/responses/PublicSandboxError"},"413":{"$ref":"#/components/responses/PublicSandboxError","description":"PUBLIC_SANDBOX_BODY_TOO_LARGE — body exceeded 32 KiB."},"429":{"$ref":"#/components/responses/PublicSandboxError"},"500":{"$ref":"#/components/responses/PublicSandboxError"}}},"options":{"summary":"CORS preflight for the public-sandbox execute endpoint","description":"204 with the public-sandbox CORS posture.","tags":["Sandbox"],"operationId":"publicSandboxActionsExecuteOptions","security":[],"responses":{"204":{"$ref":"#/components/responses/PublicSandboxOptionsPreflight"}}}},"/api/v1/sandbox/public/explain":{"post":{"summary":"Public sandbox explainer (zero-auth, IP rate-limited)","description":"Unauthenticated. Per-IP rate limit 100 requests / hour. POST body capped at 32 KiB. Resolves a structured Apier `error_code` into a Norwegian-bokmål Explanation envelope. Optional `context.org_number` is gated to 999999999. PROVENANCE EXEMPT.","tags":["Sandbox"],"operationId":"publicSandboxExplain","security":[],"parameters":[{"$ref":"#/components/parameters/CorrelationIdHeader"}],"requestBody":{"required":true,"description":"UTF-8 body capped at 32768 bytes.","content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["error_code"],"properties":{"error_code":{"type":"string"},"context":{"type":"object","additionalProperties":false,"properties":{"org_number":{"type":"string","pattern":"^\\d{9}$"},"scope":{"type":"string"},"role":{"type":"string"},"field":{"type":"string"},"upstream_system":{"type":"string"}}}}}}}},"responses":{"200":{"description":"Explanation returned. Same envelope shape as /api/v1/sandbox/explain.","content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["success","data","_meta"],"properties":{"success":{"const":true},"data":{"type":"object","additionalProperties":false,"required":["explanation"],"properties":{"explanation":{"type":"object"}}},"_meta":{"$ref":"#/components/schemas/SandboxMeta"}}}}}},"400":{"$ref":"#/components/responses/PublicSandboxError"},"413":{"$ref":"#/components/responses/PublicSandboxError","description":"PUBLIC_SANDBOX_BODY_TOO_LARGE — body exceeded 32 KiB."},"429":{"$ref":"#/components/responses/PublicSandboxError"},"500":{"$ref":"#/components/responses/PublicSandboxError"}}},"options":{"summary":"CORS preflight for the public-sandbox explain endpoint","description":"204 with the public-sandbox CORS posture.","tags":["Sandbox"],"operationId":"publicSandboxExplainOptions","security":[],"responses":{"204":{"$ref":"#/components/responses/PublicSandboxOptionsPreflight"}}}}},"webhooks":{"apierWebhookDelivery":{"post":{"summary":"Outbound webhook delivery (Apier → consumer)","description":"PR-087b — REVERSE webhook. Apier POSTs this request to the URL the consumer registered via POST /api/v1/subscriptions when a change-archive event matches the subscription's filter. SDK generators that consume this OpenAPI 3.1 document SHOULD generate a typed RECEIVER (handler) for this operation, not a CLIENT. Verification: HMAC-SHA256 over `<timestamp>.<raw_body>` using the 64-character hex `webhook_secret` VERBATIM as the key — do NOT hex-decode. Compare against the v1 segment of `X-Apier-Signature` with a constant-time compare. Reject when `|now - t|` exceeds 300 seconds (replay protection). 7-attempt exponential-backoff schedule (1m / 5m / 15m / 1h / 6h / 24h between attempts) before abandonment; auto-disable after 6 consecutive 4xx responses (excluding 408 / 429).","operationId":"apierWebhookDelivery","tags":["Subscriptions"],"parameters":[{"in":"header","name":"X-Apier-Signature","required":true,"schema":{"$ref":"#/components/schemas/WebhookDeliveryHeaders/properties/X-Apier-Signature"},"description":"Stripe-style HMAC envelope. Verify before parsing the body."},{"in":"header","name":"X-Apier-Correlation-Id","required":true,"schema":{"$ref":"#/components/schemas/WebhookDeliveryHeaders/properties/X-Apier-Correlation-Id"}},{"in":"header","name":"User-Agent","required":true,"schema":{"$ref":"#/components/schemas/WebhookDeliveryHeaders/properties/User-Agent"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookDeliveryEvent"}}}},"responses":{"200":{"description":"Receiver acknowledged the delivery. Apier resets the consecutive-4xx counter and drops the queue row."},"400":{"description":"Receiver rejected the payload (e.g. signature mismatch, bad JSON). Apier counts toward the 6-consecutive-4xx auto-disable threshold (excluding 408 / 429)."},"408":{"description":"Receiver explicitly asks Apier to retry later (request timeout). Apier RESETS the consecutive-4xx counter (this isn't a deliberate rejection — the receiver is signalling backpressure, not a misconfiguration), schedules the next attempt via the standard backoff curve, and reuses the existing webhook_secret."},"429":{"description":"Receiver explicitly asks Apier to slow down (rate limit). Apier RESETS the consecutive-4xx counter (treated as backpressure, not as a 4xx rejection — receivers asking us to back off don't burn the auto-disable counter) and schedules the next attempt via the standard backoff curve. Implementers MAY surface a `Retry-After` header; Apier currently follows its own backoff schedule regardless of the header."}}}}},"tags":[{"name":"Infrastructure","description":"Non-product endpoints (health checks, discovery)."},{"name":"Actions","description":"Filing-action surface — POST /api/v1/actions/execute. Two discriminated behaviours on the same endpoint: `?dry_run=true` runs validation only (PR-073, no upstream side effects); omitting the param submits to Altinn / Skatteetaten / NAV (PR-077, requires `Idempotency-Key` + `X-Approval-Token` headers). Both paths gate on the `read:actions` scope; the approval token is the meaningful gate for live-execute. v1 supports `mva_melding` only on the live path; `a_melding` is gated on the live NAV integration (a_melding dry-run validation IS supported)."},{"name":"Admin","description":"Administrative endpoints for managing consumers and API keys. Requires ADMIN_API_KEY."},{"name":"Auth Gateway","description":"Delegation endpoints that broker Maskinporten + Altinn on behalf of the consumer."},{"name":"Altinn","description":"Altinn-sourced authorisation surface — actor-capacity resolution, role expansion, and the underlying delegation read-side. Distinct from `Auth Gateway` (which writes System User delegations); this tag covers READ paths over actor → org → role mappings."},{"name":"Brreg","description":"Brønnøysund Enhetsregisteret context surface — agent-shaped company profile reads (PR-MCP-04). NLOD-licensed public data; role codes only, never personal identifiers (Rule 11 two-layer defense)."},{"name":"Company","description":"Reads over the Registry Engine's two-tier company model (Tier 1 public Brønnøysund data + Tier 2 gated commercial metrics)."},{"name":"Public","description":"Free, zero-auth endpoints. Rate-limited per-IP (soft cap 1000/min). Distribution infrastructure, not revenue — these exist so agents can discover the API surface and answer baseline regulatory questions without credentials."},{"name":"Discovery","description":"Agent-first discovery manifest. `/v1/capabilities` is the machine-readable menu of every callable capability; `llms.txt` and `workflows.json` are the prose and recipe counterparts."},{"name":"Changes","description":"Cross-source change archive — every detected created/updated/deleted event from Brreg, Altinn, DigDir, Norges Bank, NAV Aa-registeret, and Skatteetaten Tier 2 sub-results (MVA-register + Skatteoppgjør). Category B (read:changes scope, per-tier rate limit). Cursor-paginated for stable iteration over a moving stream."},{"name":"Privacy","description":"Zero-auth GDPR transparency endpoints — Article 15 access requests via DSR. Rate-limited per IP (10/min, lower than Category A's 1000/min) so the surface cannot be used as a name-enumeration bulk-export vector."},{"name":"Sandbox","description":"Zero-auth, deterministic, CORS-open mirrors of every Category B company endpoint (PR-074). Returns synthetic Norwegian company fixtures so agents can test failure-flows without provisioning an Apier API key, without hitting Altinn test infra, and without polluting production audit / provenance chains. `security: []` on every operation overrides any global Bearer requirement. Reserved test orgs (999000001-005), reserved error orgs (999000901-904), and `?simulate_error=<code>` are documented on `/api/v1/capabilities`. Determinism contract: same input → byte-equivalent response, EXCLUDING `_meta.response_timestamp`. See DECISIONS.md `PR-074 Sandbox Determinism Contract` for the full agent-facing contract."},{"name":"Subscriptions","description":"Pro-tier change-detection webhook subscriptions. Consumers register a webhook URL + filter; Apier fires HMAC-SHA256-signed deliveries when matching change-archive events appear. SSRF-resistant (private CIDRs incl. RFC 6598 CGNAT, .local / .internal suffixes, 169.254.x.x metadata endpoints, IPv4-mapped IPv6, trailing-dot FQDN spellings — all blocked at create AND delivery time); the TCP connection pins the validator-approved IP so a hostile DNS server can't substitute a private address between validate and connect; 7-attempt exponential backoff (initial + retries at 1m / 5m / 15m / 1h / 6h / 24h) before abandonment; auto-disables after 6 consecutive 4xx (excluding 408 / 429). Webhook secrets are returned ONCE on creation and stored encrypted-at-rest with key-version support."},{"name":"Account","description":"Consumer self-serve account management — sign-up, magic-link request, key issuance. The /api/v1/account/signup endpoint is `security: []` on purpose: an unauthenticated visitor can request a magic link without holding any prior credential."}]}