Skip to content
Apier

Which token do I need, Maskinporten or Altinn, and how do I exchange one for the other?

It depends on which Altinn 3 surface you are calling. Dialogporten's documented end-user and service-owner modes accept a Maskinporten token as issued, including the token that identifies a system user. Altinn Apps and the classic platform APIs do not: there you first call GET /authentication/api/v1/exchange/maskinporten with your Maskinporten token as the Bearer credential, receive an Altinn token that copies your scopes and adds Altinn-specific claims, and use that token on the API. Apier resolves this decision for you behind one API key, so your code never chooses between the two.

A fork with one starting point and two destinations. On the left, a Maskinporten token issued to your client. The top branch runs directly to Dialogporten, whose documented end-user and service-owner modes accept the token as issued, including system user tokens. The bottom branch runs through the exchange endpoint at exchange/maskinporten, producing an Altinn token that adds an AuthenticationLevel claim, and only then reaches the Apps and platform APIs.Accepted as issuedExchange firstMaskinporten tokenissued to your clientDialogportenend-user and service-owner modesExchange endpointexchange/maskinportenAltinn tokenadds AuthenticationLevelAppsplatform APIs
One token, two routes. The fork is decided by the API you are calling, not by anything about your client: the same Maskinporten token is either accepted as issued or traded for an Altinn token first.

When is a Maskinporten token enough on its own?

Start from what the token is. Maskinporten authenticates your organisation and issues a short-lived JWT carrying the scopes your client has been granted. That token proves who you are to any API whose documentation states it accepts Maskinporten tokens directly. Dialogporten documents exactly that: an end-user system can call it with a Maskinporten token directly, including the token variant that identifies a system user acting for a customer, and service-owner systems use Maskinporten tokens with the exchange as an option rather than an obligation.

The classic platform is the other half of the split. Altinn Apps and the platform APIs behind them expect an Altinn token: the documentation is explicit that a Maskinporten token has to be validated and replaced before those APIs are called. The reason is practical rather than ceremonial. The Altinn token carries claims the platform needs and Maskinporten does not issue, such as the resolved organisation number in Altinn's own fields and an authentication level, and the platform's authorisation rules are written against those claims.

So the decision rule is short: look up which surface you are integrating against before you write the token plumbing. If it is Dialogporten or another surface documented as accepting Maskinporten tokens, you are done after the first token call. If it is an app or a platform API, budget for the exchange described next. The broader question of how the Maskinporten leg itself works, certificates, assertions and caching, is owned by the authentication guide, and this page assumes that leg already works.

How does the exchange call work?

It is one GET request. You call /authentication/api/v1/exchange/maskinporten on the Altinn platform host for your environment, with the Maskinporten token in the Authorization: Bearer header and nothing else. The endpoint validates the incoming token, then mints and returns a new JWT signed by Altinn. In the test environment the host is platform.tt02.altinn.no; in production it is platform.altinn.no. The environments are separate trust domains, which matters for the failure modes below.

The output token is worth reading once in a debugger, because it explains why the exchange exists. Your scopes are copied across unchanged, so the exchange never widens or narrows what you may do. On top of them the converter adds the organisation fields Altinn resolves for you and an AuthenticationLevel claim. That last claim is the one the raw Maskinporten token does not carry, and it is what authorisation policies that require a minimum authentication level evaluate.

Treat the result as a short-lived credential in the same loop as the Maskinporten token that produced it. Mint, exchange, cache until shortly before expiry, repeat. Nothing about the exchange is a one-time registration, and nothing persists server-side between calls: if you hold a valid Maskinporten token you can always mint a fresh Altinn token from it. The same clock discipline the assertion step needs applies here, since an expired input token fails the exchange rather than degrading gracefully.

The common failure modes around the token exchange, what each one looks like from the caller's side, and the fix that actually addresses it.
FailureWhat you seeFix
Expired Maskinporten token401 from the exchange endpoint. Maskinporten tokens live for minutes, and the exchange validates before minting.Mint a fresh Maskinporten token and retry. Cache tokens with a safety margin before expiry.
Expired or mis-timed assertionThe failure happens one step earlier: Maskinporten itself rejects the token request.Synchronise the signing host's clock and keep the assertion window short rather than widening it.
Wrong environment401 from the exchange: a token minted in the test environment presented to production, or the reverse.Pair the Maskinporten environment with the matching platform host, TT02 with TT02 and production with production.
Raw token on an exchange-requiring APIThe exchange succeeded or was skipped, and the API call itself is rejected as unauthorised.Exchange first and present the Altinn token, so the authentication level claim is present when authorisation runs.

Why does a direct system user token get rejected?

This is the failure that costs integrators the most time, because every individual step looks correct. You hold a valid Maskinporten token for a system user, the delegation exists, the scopes are right, and the API still answers with an authorisation error. The missing piece is usually not authority but information: the authorisation decision point evaluates claims on the token it is given, and a raw Maskinporten token does not carry the authentication level claim that the exchanged Altinn token does.

When a policy requires a minimum authentication level and the token presents none, the request is rejected even though the underlying delegation is valid. From the outside that reads as a permissions bug, and teams respond by re-checking delegations that were never broken. The fix is mechanical: route the token through the exchange endpoint first, and make the exchanged token the only one your Altinn client ever attaches to app and platform calls.

If you are seeing 403 or 500 answers around system user requests more broadly, approval failures, delegation checks and list endpoints have their own distinct failure cluster, and the system user troubleshooting guide walks each symptom to its cause. The token-shaped subset of those errors resolves here; the delegation-shaped subset resolves there.

Make the first call

The curl below is the exchange itself, in TT02, assuming you already hold a Maskinporten token. The TypeScript sample is the brokered route: one Apier key, a dry-run action, and the token decision happening on the other side of the call.

# The exchange itself, in the TT02 test environment. One GET,
# the Maskinporten token as the Bearer credential, an Altinn JWT back.
curl -s https://platform.tt02.altinn.no/authentication/api/v1/exchange/maskinporten \
  -H "Authorization: Bearer $MASKINPORTEN_TOKEN"
// The brokered equivalent: Apier resolves which token the upstream
// call needs, so your code sends one key and never sees either token.
const res = await fetch(
  "https://www.apier.no/api/v1/actions/execute?dry_run=true",
  {
    method: "POST",
    headers: {
      "content-type": "application/json",
      Authorization: `Bearer ${process.env.APIER_API_KEY}`,
    },
    body: JSON.stringify({
      org_number: "999999999",
      action_type: "mva_melding",
      period: "2026-T1",
      payload: {},
    }),
  },
);

if (!res.ok) {
  // Every non-2xx answers the same structured envelope.
  const { error_code, explanation } = await res.json();
  throw new Error(`${error_code}: ${explanation.summary}`);
}

const { data } = await res.json();
console.log(data.outcome.all_passed, data.outcome.checks);

Frequently asked questions

Is an Altinn token a different credential from Maskinporten?
It is the same identity, re-minted. The exchange endpoint validates your Maskinporten token and issues a new JWT that copies the scopes and adds Altinn-specific claims, including the organisation number and an authentication level. You never apply for an Altinn token separately; you always derive it from a Maskinporten token you already hold.
Which Altinn APIs accept a Maskinporten token directly?
Dialogporten does: an end-user system can present a Maskinporten token identifying a system user, and service-owner systems use Maskinporten tokens with the exchange as an option rather than a requirement. Altinn Apps and the classic platform APIs are the opposite case: they expect the Altinn token minted by the exchange endpoint, not the raw Maskinporten token.
Why does the exchange call itself return 401?
The exchange validates the incoming token before minting anything, so a 401 there means the Maskinporten token failed that validation. The usual causes are a token that has already expired, since Maskinporten tokens live for minutes, or a token minted in one environment and presented in another, since the test and production platforms trust different issuers.
Do I exchange once and keep the Altinn token?
No. The Altinn token inherits a short lifetime, so treat the exchange as part of the token loop rather than a setup step: mint a Maskinporten token, exchange it, cache the result until shortly before expiry, then run the loop again. The same caching discipline that applies to Maskinporten tokens applies to what the exchange returns.
Does Apier expose the exchange to me?
No, it removes the decision entirely. Your application authenticates to Apier with one API key. When a request needs a government upstream, Apier's side holds the Maskinporten client, performs any exchange the target API requires, and sends the right token. Your code cannot pick the wrong token because it never picks a token at all.