What Altinn authentication level does my call need, and why do I get 403 at level 2?
Every Altinn session and token carries an authentication level that reflects how the caller authenticated, and a service's policy can declare a minimum it will accept. A refusal at level 2 means exactly that: the credential you presented sits below the minimum the policy demands, so the request is refused regardless of how valid your rights and delegations are. Machine callers meet the same wall in a sharper form, because a raw Maskinporten token carries no authentication level claim at all; exchanging it for an Altinn token adds the claim, with 3 as the documented value for an organisation. Level 4 surfaces have no documented machine path, so those steps belong to a human.
What are Altinn authentication levels?
A measure of how strongly the caller proved who it is, attached to the session or token at authentication time. The weakest methods sit at the bottom: a username-and-password login gives access to services requiring level 1. Stronger electronic IDs authenticate at higher levels, and an ID-porten token records the result in its acr claim, with Level3 as the documented example. The level is not a permission: it says nothing about what the caller may do, only how firmly Altinn knows who is calling.
Machines have levels too, and this is where the model bites integrators. Enterprise certificate authentication provides security level 3, and the documentation is explicit that it provides only that. The Altinn token minted by the exchange endpoint makes the value visible: the documented organisation token carries an AuthenticationLevelclaim of 3. So a machine caller's ceiling is level 3, and no scope, delegation or certificate strength raises it.
Level 4 exists above that, reached with the strongest electronic IDs. For the documented message-service case, reading a level 4 service is only possible through ordinary ID-porten login, and machine integration is not possible there. That single fact decides an architecture question: any workflow that touches a level 4 surface needs a human step, and pretending otherwise produces a refusal no code change can fix.
How does a service declare its minimum, and what happens below it?
In Altinn 3 the requirement lives in the service's authorisation policy. The decision returned for a request can carry an obligation whose attribute category is urn:altinn:minimum-authenticationlevel, with an integer value; the documented example sets it to 2. The shape matters more than the number: the minimum is a property of the resource's policy, chosen by the service owner, so two services behind the same host can demand different levels and answer the same credential differently.
When the credential sits below the declared minimum, the request is refused even though the rights check would have passed. That is the answer to the question in this page's title: a 403 at level 2 is the policy demanding more certainty about who is calling than your credential provides. The fix is never in the request payload and always on the credential side, which is what separates this failure from every other 403 in the troubleshooting silo.
How the refusal surfaces is less tidy than the model. Integrators observe that a level shortfall does not always arrive as a labelled denial: it can surface as a generic error page, or as a server error that says nothing about authentication levels. Nothing in the official pages promises those shapes, so treat them as observations rather than contract, and adopt the debugging rule they imply: on a level-gated surface, an unexplained rejection is a level problem until the credential's claims prove otherwise.
| Symptom | Likely cause | Fix |
|---|---|---|
| 403 at level 2 on a portal-backed flow | The login method sits below the minimum the service's policy declares. | Re-authenticate with a stronger electronic ID before retrying the service. |
| Valid delegation, machine call refused | A raw Maskinporten token was presented; it carries no AuthenticationLevel claim. | Exchange the token first and present the Altinn token on the call. |
| Same call passes as a person, fails as a machine | The service requires level 4, and machine paths reach only level 3. | Route that step to a human with a level 4 electronic ID; no code change fixes it. |
| Generic error page instead of a denial (observed) | A level shortfall surfacing without a labelled refusal. | Check the credential's level claims before debugging anything else. |
| 500 where a refusal makes sense (observed) | A refusal-shaped condition surfacing with a server-error status. | Exhaust the level and authority hypotheses before treating it as an outage. |
How do I debug a level failure instead of a permission failure?
Separate the two gates first. A permission failure is about what the caller may do, and its trail runs through delegations, packages and scopes; that cluster is owned by the system user troubleshooting guide and the delegation troubleshooting guide. A level failure is about how the caller authenticated, and its trail is one item long: what level claim does the presented credential actually carry.
For machine callers that one item almost always resolves the case. Decode the token you are actually sending and look for the AuthenticationLevel claim: a raw Maskinporten token will not have one, and the fix is to route it through the exchange first, which the token exchange guide owns end to end. If the claim is present and the service still refuses, you are either below a higher minimum or on a level 4 surface, and the resolution is a stronger credential or a human step rather than another retry.
The Apier-shaped version of this discipline is to run the checks before the call that would fail them. The dry-run mode of the actions endpoint validates a filing action against five named checks, including whether a system user delegation is active and whether it carries the required scopes, without submitting anything upstream. What would have been an unexplained rejection at the far end arrives instead as a named failing check with an explanation attached, which is the difference between a production incident and a checklist item.
Make the first call
The sandbox call answers who may act for a company at all, with no key. The TypeScript sample runs the dry-run pre-flight: the named checks for a filing action, evaluated without anything being submitted, so a gap arrives labelled instead of as a 403 at the end of the chain.
# Zero-auth sandbox: who may act for a company, from the registry.
curl -s https://www.apier.no/api/v1/sandbox/public/company/999999999/authority// Pre-flight: run the named validation checks for a filing action
// against apier.no with one Apier key, before anything faces the wall.
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();
// Each check reports by name: company_exists, system_user_authorised,
// scopes_delegated and the rest, so a gap arrives labelled.
for (const check of data.outcome.checks) {
console.log(check.check, check.passed);
}Frequently asked questions
- Where does my authentication level actually come from?
- From how the caller authenticated, not from what it is allowed to do. A username-and-password login sits at level 1, and stronger electronic IDs authenticate at higher levels. On the machine side, enterprise certificate authentication provides level 3, and the Altinn token minted by the exchange endpoint carries an AuthenticationLevel claim, with 3 as the documented value for an organisation token. The level travels with the session or token; rights and delegations are checked separately.
- My delegation is valid. Why is the call still refused?
- Because the level check and the rights check are different gates, and passing one says nothing about the other. The common machine-side shape is a raw Maskinporten token sent to a surface whose policy requires a minimum level: the raw token carries no AuthenticationLevel claim at all, so there is nothing for the policy to accept. Exchanging the token first adds the claim, and the same delegation then passes.
- Can a machine reach a service that requires level 4?
- Not in the documented message-service case. The documentation states that reading message services at security level 4 is only possible through ordinary ID-porten login, and that machine integration is not possible there, since certificate authentication provides only level 3. When a flow hits such a service, the honest design is a handover: a human authenticates with a level 4 electronic ID and performs that one step.
- Why do I sometimes get a generic error page or a 500 instead of a clear denial?
- Integrators observe that a level shortfall does not always surface as a labelled refusal: the answer can arrive as a generic error page or a server error with no mention of authentication levels. None of that is documented behaviour to rely on. The practical rule is to treat an unexplained rejection on a level-gated surface as a level problem first, and check what your credential carries before treating it as an outage.
- How does Apier keep me from hitting the level wall blind?
- By running the checks as a pre-flight with named verdicts. POST /api/v1/actions/execute with dry_run=true validates a filing action against five checks, including whether the company exists, whether a system user delegation is active, and whether the delegation carries the required scopes, without submitting anything upstream. A gap arrives as a named failing check with an explanation, not as an unexplained rejection at the far end of the chain.