Actions

Validate (dry-run) or submit (live) a filing action

Two discriminated behaviours on the same endpoint, selected by the ?dry_run=true query parameter.

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).

The five checks (run in order, all reported, never short-circuited):

  1. company_exists — does org_number exist in Brønnøysundregistrene?
  2. system_user_authorised — is there an active System User delegation for this consumer + this org_number?
  3. scopes_delegated — does the delegation carry the upstream Maskinporten scopes the action requires? (For mva_melding: both altinn:instances.read and altinn:instances.write — the Altinn 3 submission scope pair from RequiredScopesByAction, which Skatteetaten names in place of the retired 2022 scope skatteetaten:mvameldinginnsending; they are end-user-system scopes obtained at ID-porten login, so in 2026 a human authenticates with ID-porten to submit the return the agent prepared. Distinct from the skatteetaten:mva tier-access baseline shown on /v1/auth/permissions; a_melding live submission is paused — transitioning to Skatteetaten direct integration; dry-run validation remains supported.)
  4. data_format_valid — does the payload pass the per-action Zod schema?
  5. deadline_in_future — is the obligation deadline still in the future at clock time?

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 live submission is paused (transitioning to Skatteetaten direct integration) and returns 403 PLAN_INSUFFICIENT. MOCK-GATED: with ALTINN_MODE=mock (the default — see /docs/capability-status) the submission is served by Apier's deterministic mock Altinn adapter rather than a real government call; the response envelope is shaped identically to a live submission, but altinn_receipt_id is prefixed MOCK-ALT- and the receipt's government_response_raw carries a nested _mock: true marker. The live adapter activates only once ALTINN_MODE=live is configured for a production deployment.

Live-execute applies THREE additional contracts over dry-run:

  • Idempotency-Key header is REQUIRED (a missing key returns 400 IDEMPOTENCY_KEY_REQUIRED — network retries without the same key would double-submit).
  • 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 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.
  • 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.

Successful 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.

DISCRIMINATED 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.

IDEMPOTENCY — Idempotency-Key is OPTIONAL on dry-run and REQUIRED on live-execute. 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. 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.

BODY SIZE — bounded at 256 KB. Beyond that the request returns 413 PAYLOAD_TOO_LARGE.

The 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).

post/api/v1/actions/execute

Request

  • Base URL: https://www.apier.no
  • URL: https://www.apier.no/api/v1/actions/execute
  • Auth: HTTP bearer

Query parameters

dry_runstring

Mode discriminator. 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.

Headers

Idempotency-Keystring

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 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. 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. 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).

X-Approval-Tokenstring

Single-use approval token. REQUIRED for live-execute; ignored on dry-run. Mint via POST /api/v1/auth/approval-token after a human operator confirms the action.

Consumption semantics (correction per consume_approval_token_atomic actual behavior — only the success path runs the CAS UPDATE on approval_tokens.used_at):

Token NOT consumed (retry-safe with same token, within TTL):

  • 422 VALIDATION_FAILED — live preconditions failed before the consume RPC ran. Fix the inputs and resubmit with the same token.
  • 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.
  • 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.
  • 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.

Token IS consumed (single-use rule — retries require a fresh token):

  • 200 (success) — happy path, the CAS UPDATE fired and used_at is now set.
  • 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.
  • 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.
  • 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.

The 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 and MISMATCH branches are never consumptive at the RPC layer. The single-use-on-failure rule applies only after a successful consume. A token that does not exist answers APPROVAL_TOKEN_INVALID, so the response never discloses whether a token exists.

Request body

OR

Response

Operation completed. Two discriminated body shapes: 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.

successtrue required

Example response

{
  "data": {
    "signed_receipt": {
      "org_number": "999999999",
      "government_response_raw": {
        "hash": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
      },
      "government_response_hash": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
    }
  },
  "_meta": {
    "rulebook_version": "v1.0.0",
    "source": "apier.no",
    "data_source": "Brønnøysund Enhetsregisteret + Apier Universal Rulebook",
    "schema_version": "1.0.0",
    "legal_basis": "NLOD — public registry reuse",
    "response_timestamp": "2026-04-24T12:00:00.000Z",
    "response_hash": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
    "source_snapshot_id": "brreg-2026-04-24T02:00:00.000Z",
    "cost_nok": 0.5,
    "wallet_remaining_nok": 9.5,
    "spend_cap_remaining_nok": 12.5
  }
}

Changes

No recorded changes to this operation across all 1 revision of this API.