MVA filing workflow step by step
From obligation discovery through dry-run validation, plus the gated live-submission contract.
[Cite this as: Apier.no Docs v0.1.0 — last updated 2026-05-25]
This guide walks through the MVA (merverdiavgift / VAT) filing workflow using the Apier API. Obligation discovery and dry-run validation are available today; live submission is gated (see Capability status).
Prerequisites
- An API key with the
read:actionsscope - The organisation number of the company to file for
- For the eventual live path: a delegated Altinn system user with MVA filing access
Step 1 — Discover the obligation
GET /api/v1/company/{org}/obligations HTTP/1.1
Authorization: Bearer apr_test_<your_key_here>Find the MVA obligation in the response (its obligation_id identifies the MVA filing rule) and read its evaluation_result and deadline. If evaluation_result is insufficient_data, the company has not delegated Altinn access to Apier — the engine cannot evaluate turnover-dependent MVA rules until it does (see Authentication and Understanding obligations).
Step 2 — Dry-run the submission
Always dry-run first. The dry-run validates the payload against the action's preflight checks without submitting anything. Note dry_run is a query parameter, and the action type is mva_melding:
POST /api/v1/actions/execute?dry_run=true HTTP/1.1
Authorization: Bearer apr_test_<your_key_here>
Content-Type: application/json
{
"org_number": "999999999",
"action_type": "mva_melding",
"period": "2026-T1",
"payload": {}
}The period uses Norwegian MVA terminer (YYYY-Tn, n = 1–6), not calendar quarters. A successful dry-run returns HTTP 200 with an outcome object — outcome.all_passed tells you whether every preflight check passed. A 200 with all_passed: false is still the success path: the dry-run itself succeeded, and individual check verdicts are content, not transport errors.
Step 3 — Request an Approval Token (live path)
Live submission requires a single-use, human-approved token. Mint one:
POST /api/v1/auth/approval-token HTTP/1.1
Authorization: Bearer apr_test_<your_key_here>
Content-Type: application/json
{
"org_number": "999999999",
"action_id": "mva_melding:999999999:2026-T1"
}The body carries org_number and action_id (the identifier of the action being approved); an optional expires_in_seconds shortens the lifetime. The token is single-use and short-lived — pass it promptly (the live path rejects an expired token with APPROVAL_TOKEN_EXPIRED).
Step 4 — Submit (gated)
⚪ Gated. Live filing submission requires Maskinporten production validation and Altinn scope approval, and is not yet enabled — see Capability status. The contract below is what the call will use once it ships.
POST /api/v1/actions/execute HTTP/1.1
Authorization: Bearer apr_test_<your_key_here>
Idempotency-Key: <unique-per-submission>
X-Approval-Token: <token-from-step-3>
Content-Type: application/json
{
"org_number": "999999999",
"action_type": "mva_melding",
"period": "2026-T1",
"payload": {}
}Both the Idempotency-Key and X-Approval-Token headers are required on the live path — the idempotency key prevents a network retry from double-submitting, and the approval token enforces the human-in-the-loop gate. A successful response carries a signed receipt with the upstream altinn_receipt_id and a _meta.rulebook_version so you can audit which rule version was active at submission time.
Maskinporten Key Rotation
Operator runbook for rotating the production Maskinporten signing keypair (MASKINPORTEN_KID + MASKINPORTEN_PRIVATE_KEY).
Going live
Live execution is currently off. What the capabilities flag means, what changes when filings go live, your client-side pre-flight checklist, and the external gates outside your control.