---
title: "Guided MVA write-loop rehearsal (mock-gated, real HITL mechanics)"
method: POST
path: "/api/v1/sandbox/rehearsal/execute"
tags: ["Sandbox"]
---

# Guided MVA write-loop rehearsal (mock-gated, real HITL mechanics)

`POST /api/v1/sandbox/rehearsal/execute`

## Request

- Base URL: `https://www.apier.no`
- URL: `https://www.apier.no/api/v1/sandbox/rehearsal/execute`
- Auth: HTTP bearer

Customer-facing rehearsal of the full production MVA write loop, driving the guided walkthrough on /sandbox. Composes the PRODUCTION modules — pending-action state machine, approval-token atomic consume, receipt signer/persister — with two deliberate differences from `/api/v1/actions/execute`: the human-in-the-loop suspension branch is ALWAYS ON route-locally (production gates it behind the `HITL_ENFORCE` flag, fail-safe OFF), and the Altinn submitter is the HARDCODED mock (`ALTINN_MODE` is never read), so this surface can NEVER reach a government system. Receipts carry `MOCK-ALT-…` ids and `government_response_raw._mock: true`.

TWO BRANCHES, selected by the `X-Approval-Token` header:

**No token** — the validated request is staged as a REAL `pending_actions` row (15-minute TTL, readable via GET `/api/v1/actions/pending/{id}`) and suspended with 412 HITL_PENDING_APPROVAL. The 412 is idempotency-cached: re-sending with the SAME `Idempotency-Key` replays the SAME `pending_action_id` (the idempotency replay guarantee), never a second pending row.

**Token present** — the token (minted via the production POST `/api/v1/auth/approval-token`, bound to `(org_number, action_id: "filing.submit")`) is consumed EXACTLY ONCE through the same atomic CAS RPC production uses (`consume_approval_token_atomic`); there is no bypass path. On success the mock submit → HMAC-sign → persist pipeline runs with the production two-row audit pattern (`submit_mva_started` pre-insert, then `submit_mva` / `submit_mva_failed` outcome rows, all carrying `details.surface: "sandbox_rehearsal"`). Rejected tokens return the production error codes verbatim: APPROVAL_TOKEN_INVALID (existence-disclosure-safe NOT_FOUND collapse) / APPROVAL_TOKEN_USED / APPROVAL_TOKEN_EXPIRED / APPROVAL_TOKEN_MISMATCH.

AUTH — a REAL Apier API key is required (the synthetic `apier_sandbox_test_*` bearer is rejected 401: every step writes real rows keyed to your account). Required scope `read:actions` — the same scope as the production execute surface; default signup keys (`read:*`) qualify. Unlike the other `/v1/sandbox/*` routes this endpoint is scope-gated and idempotency-wrapped, because it writes REAL pending/audit/receipt rows — it is the deliberate stateful exception on the sandbox surface.

`?dry_run` is NOT supported here (400 VALIDATION_FAILED) — the walkthrough's validation step targets the production dry-run, which reports all five precondition checks. The rehearsal deliberately skips the precondition engine so consumers without an active delegation can still rehearse the write mechanics.

`?simulate_error=<missing_delegation|invalid_token|validation_error|scope_missing>` returns a canned production-shaped error envelope BEFORE any state is written — rehearse your error handling without dirtying loop state. The upstream-failure simulation tokens are deliberately absent: the submitter here is the local deterministic mock.

Every response carries `_meta.is_sandbox: true`, and EVERY response schema on this operation requires the marker — the 401 / 409 / 412 / 429 wrap the shared production contracts (same headers, same envelope, composed via allOf) with the marker requirement added, so the schema contract matches the wire contract on all nine documented statuses. Provenance: per the sandbox carve-out, NO `provenance_log` row is written for rehearsal responses; `_meta.response_hash` is still stamped for client-side verification.

## Query parameters

- `simulate_error` 'missing_delegation' | 'invalid_token' | 'validation_error' | 'scope_missing'

## Headers

- `Idempotency-Key` string, required
- `X-Approval-Token` string

## Request body

- object
  - `org_number` string, required: 9-digit Norwegian organisation number. On the token branch it must match the org_number the approval token was minted for, or the consume returns 403 APPROVAL_TOKEN_MISMATCH.
  - `action_type` 'mva_melding', required: Closed to `mva_melding` — the one action the rehearsal (and the production live branch) supports. The REST literal, never the MCP tool name.
  - `period` string, required: MVA period — bimonthly terminer (YYYY-Tn, n=1..6), annual (YYYY-A), or monthly (YYYY-MM). Same grammar as the production execute surface.
  - `payload` MvaMeldingPayload, required: 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; the live-execute path adds semantic checks.
    - `total_revenue_nok` string, required: 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` string, required: Non-negative decimal NOK.
    - `total_incoming_vat_nok` string, required: Non-negative decimal NOK.
    - `exchange_rate_used` object: Optional currency-conversion snapshot when invoices reference non-NOK transactions. When present, both fields are required.
      - `currency` string, required: ISO 4217 currency code (3 uppercase letters).
      - `rate` string, required: 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.

## Response `200`

Token consumed and the mock submit → sign → persist pipeline completed. Same data shape as the production live-execute 200 minus the reliability envelope (`execution_guarantee` / `outcome` — the rehearsal composes no orchestrator). `altinn_receipt_id` is the deterministic `MOCK-ALT-…` id; `signed_receipt.government_response_raw._mock` is `true`; `signed_receipt.audit_log_id` + `signed_receipt.correlation_id` are the keys into the append-only audit evidence the walkthrough's final step renders.

- object
  - `success` true, required
  - `data` object, required
    - `dry_run` false, required: Always `false` on a successful execute response — confirms the rehearsal performed a real (mock) submission rather than a dry-run validation pass. The `dry_run=true` variant is served by POST /api/v1/sandbox/rehearsal/execute?dry_run=true and returns 422 VALIDATION_ONLY_DRY_RUN without staging a pending action.
    - `altinn_receipt_id` string, required: Mock receipt id, always prefixed `MOCK-ALT-` — never produced by a live path.
    - `receipt_id` string, uuid, required: Apier-side receipts table UUID.
    - `submitted_at` string, date-time, required: RFC 3339 completion timestamp.
    - `signed_receipt` SignedReceipt, required: HMAC-signed government submission receipt. Returned by `/api/v1/actions/execute` 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. The `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). The `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.
      - `altinn_receipt_id` string, required: Upstream-issued receipt id (Altinn / Skatteetaten / NAV). Forwarded verbatim from the gov API response.
      - `audit_log_id` string, uuid, required: 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` string, required: 9-digit Norwegian organisation number the receipt belongs to.
      - `filing_type` string, required: 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` string, required: 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` string, uuid, required: Request-scope UUID v4. Joins `audit_log`, `provenance_log`, `evaluation_snapshots`, and `compliance_state_events` on the same forensic axis.
      - `government_response_raw` union, required: 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. The 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. When this receipt was produced by a mock submission (Apier's `ALTINN_MODE=mock` default — see the `/api/v1/actions/execute` operation description), the object carries a nested `_mock: true` boolean as the discoverable signal that no real government system was contacted.
        - object
        - unknown[]
          - unknown
        - string
        - number
        - boolean
        - LargeGovernmentResponse: 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. 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.
          - `truncated` true, required: Discriminator: always true. A non-truncated receipt carries the verbatim payload on `government_response_raw` instead of this sentinel.
          - `byte_length` integer, required: 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` integer, required: Soft cap on the inline projection size (echoed for transparency). Today: 65536 (64 KB).
          - `hash` string, required: SHA-256 of canonicalJSON(normalised payload), prefixed `sha256:`. Joins to `large_government_responses.response_hash` and matches `receipts.government_response_hash`.
          - `note` string, required: Static guidance pointing at the spill-bucket join — instructive for a human reading the row in isolation.
      - `government_response_hash` string, required: 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`.
      - `signed_at` string, date-time, required: RFC 3339 / ISO 8601 timestamp of when the receipt was signed. Caller-supplied (will pass the route's frozen time-source) so the signature is deterministic across retries.
      - `signature_algorithm` 'HMAC-SHA256-v1', required: Versioned algorithm tag for signed submission receipts. 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.
      - `signature` string, required: 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` string[], required: 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 (Major). Echoed verbatim on the wire for forensic transparency.
  - `_meta` object, required: Metadata included in every response for trust and verifiability.
    - `rulebook_version` string, required: 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.
    - `data_freshness` string, date-time, required: When this data was last fetched (ISO 8601 UTC).
    - `last_verified` string, date-time, required: When this data was last verified against source (ISO 8601 UTC).
    - `source` string, required: Origin of this data. Defaults to `apier.no`. The agent-readiness probe (`GET /api/v1/health/agent-readiness`) is the one surface that may instead emit `apier.no telemetry` — it does so when at least one of its endpoint rates came from measured live telemetry, and falls back to `apier.no` when every rate is a static default; all other endpoints always emit `apier.no`.
    - `data_source` string: Authoritative origin of the underlying data (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.
    - `served_from` 'cache' | 'live' | 'static': 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` boolean: 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` string, required: 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.
    - `legal_basis` string: Legal basis under which the response data is reused. Present on Rulebook-influenced endpoints (v1/company/{org}/obligations, /v1/company/{org}/deadlines). Common value: 'NLOD — public registry reuse'.
    - `rule_count` integer: Rule-evaluation count — total rules evaluated for this request. Present on endpoints that invoke the Rulebook evaluator (v1/company/{org}/obligations).
    - `applicable_count` integer: Count of rules whose evaluation_result is 'applicable'. Present on evaluator endpoints.
    - `not_applicable_count` integer: Count of rules whose evaluation_result is 'not_applicable'. Present on evaluator endpoints.
    - `insufficient_data_count` integer: 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` integer, nullable: 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 cache (v1/company/{org}/obligations, /deadlines, /summary, /v1/public/obligations, /v1/public/deadlines).
    - `response_time_ms` integer: 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 endpoint.
    - `response_timestamp` string, date-time: 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 (append-only response contracts). A future revision may promote it into the `required` list alongside a bulk example refresh.
    - `response_hash` string: 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.
    - `source_snapshot_id` string: 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).
    - `is_sandbox` true, required: Always true on this surface — the machine-readable rehearsal marker, present on every response including error paths. No live government-sourced response ever carries it.
    - `cost_nok` number: What this call debited from the caller's prepaid credit balance, in NOK, rendered from the integer-øre ledger (50 øre → 0.5; the ledger itself never leaves whole øre and no float arithmetic touches a balance). Present on the 2xx body of every metered company-data read — the same debit that sets the `X-Credits-Cost-Ore` header, so the two never disagree. `0` when nothing was charged: credit enforcement running dark (the response then also carries `X-Credit-Check: shadow`) or the sandbox surface. Absent on unmetered endpoints and on error envelopes — a refunded 5xx/403/404 claims no cost rather than a wrong one. Lives under `_meta`, which `response_hash` excludes, so the provenance receipt is unaffected; the route's ETag is computed before the credit wrapper stamps it, so a conditional request can still short-circuit to 304. Append-only addition.
    - `wallet_remaining_nok` number, nullable: The prepaid credit balance remaining AFTER this call's debit, in NOK (same rendering as `cost_nok`; mirrors the `X-Credits-Balance-Ore` header). `null` when no balance was read: credit enforcement running dark, or the sandbox surface. Present wherever `cost_nok` is. An agent planning a sequence of metered calls can divide this by the per-call price from GET /api/v1/pricing to know how many reads remain before a 402 INSUFFICIENT_CREDITS. Append-only addition.
    - `spend_cap_remaining_nok` number: What the calling key may still spend under its tighter spend cap (Europe/Oslo calendar day or month) AFTER this call's debit, in NOK (same rendering as `cost_nok`; never below 0). Present on the 2xx body of a metered read only when credit enforcement is live AND the key has a daily or monthly cap (set with PATCH /api/v1/account/keys/{id}); absent otherwise. When it reaches 0 the next metered call on this key returns 402 SPEND_CAP_REACHED until the cap resets. Append-only addition.

## Other responses

- `400`: Transport-level rejection before any state change. `error_code` values: `VALIDATION_FAILED` (non-JSON body; a schema violation — including an unknown top-level key, the body schema is strict, or a payload that fails the strict MvaMeldingPayload boundary; an unknown `?simulate_error` value; an unsupported `?dry_run` — the walkthrough's validation step targets the production POST /api/v1/actions/execute?dry_run=true; or invalid UTF-8) / `IDEMPOTENCY_KEY_REQUIRED` (the header is mandatory on every rehearsal call — the `?simulate_error` short-circuit is the one branch that does not require it).
- `401`: Authentication failed — missing, malformed, invalid, or revoked API key. The synthetic `apier_sandbox_test_*` bearer is rejected here too: every row the rehearsal writes is keyed to a real account, so a real key is required. Same contract as the shared 401; on this surface the envelope additionally carries the required `_meta.is_sandbox: true` marker, injected by the surface's outermost response rewrap so no short-circuit path can omit it.
- `403`: Authorization failure. `error_code` values: `SCOPE_INSUFFICIENT` (key lacks `read:actions`) / `APPROVAL_TOKEN_INVALID` (unrecognised token — existence-disclosure-safe) / `APPROVAL_TOKEN_USED` (the exactly-once CAS already consumed it) / `APPROVAL_TOKEN_EXPIRED` / `APPROVAL_TOKEN_MISMATCH` (token bound to a different org_number, action_id, or API consumer than the one this request authenticates as). Via `?simulate_error`: canned `AUTH_MISSING_DELEGATION` / `SCOPE_MISSING`, emitted before any state is written.
- `409`: Another request with this Idempotency-Key is still being processed — wait the number of seconds in Retry-After, then retry. Same contract as the shared idempotency in-progress response; on this surface the envelope additionally carries the required `_meta.is_sandbox: true` marker.
- `412`: Action staged and suspended pending human approval — on the rehearsal surface this HITL suspension branch is ALWAYS-ON (it is the loop the surface exists to demonstrate; no enforcement flag is read). The staged pending_actions row is REAL (15-minute TTL) and readable via the production GET /api/v1/actions/pending/{id}. The body is the closed four-field envelope { pending_action_id, status, expires_at, approval_url } — the validated payload is never echoed, and approval_url is built from the server-configured base URL, never the inbound Host header. Replaying the same Idempotency-Key returns this same 412 with the SAME pending_action_id (the staged suspension's replay guarantee). Unlike the shared production 412 component, the schema here additionally requires the `_meta.is_sandbox: true` marker.
- `413`: Request body exceeds 256 KB (PAYLOAD_TOO_LARGE).
- `422`: Single case on `error_code`: `IDEMPOTENCY_KEY_MISMATCH` — same Idempotency-Key re-sent with a different body.
- `429`: Rate limit reached on at least one of two independent dimensions: per-(api_key, endpoint) per-minute, or per-(api_key, org_number) per-day. The X-RateLimit-Dimension header identifies which bucket tripped; Retry-After is the binding wait. Same contract as the shared rate-limit response; on this surface the envelope additionally carries the required `_meta.is_sandbox: true` marker.
- `500`: Internal failure. `error_code` values: `INTERNAL_ERROR` (staging could not be persisted — retry with the SAME Idempotency-Key, replay-safe) / `EXECUTION_FAILED` (token-consume infrastructure error — token NOT consumed — or the local mock sign/persist pipeline failed AFTER consume — mint a new token and retry with a NEW key). No government submission can occur on any branch of this surface.

---

[API](https://skmtc.dev/apier/apis/apier-no-api.md) · [All operations](https://skmtc.dev/apier/apis/apier-no-api/llms.txt) · [OpenAPI document](https://skmtc.dev/apier/apis/apier-no-api/revisions/9ec19bc87311?raw)
