Skip to content
Apier

Why does my Altinn system user request return 403 or 500?

Almost always because of who holds what, not because of what you sent. A 403 while approving a system user request usually means the approver does not hold, or cannot delegate, one of the access packages the request names. A 403 after a successful delegation, when the target is Altinn Apps or another API that requires the exchanged Altinn token, usually means a raw Maskinporten token was sent unexchanged; APIs documented to accept system user tokens directly, such as Dialogporten, are not this failure. And some of the 500s in this flow behave like rejections wearing the wrong status code, so treat them as authority problems before treating them as outages.

Four steps in a row: a vendor request naming the access packages, the customer approval which is itself a delegation, the system user grant where packages are delegated, and the API call authorised by the policy decision point. Dashed callouts below mark where each failure surfaces: a 403 at approval when the approver lacks the package, a delegation check rejection at the grant step, and a 403 at the API call when the target API requires an exchanged Altinn token and received a raw Maskinporten token.Vendor requestnames the packagesCustomer approvalis itself a delegationSystem user grantpackages delegatedAPI callPDP authorises403: approver lacks the packagedelegation check rejects403: Altinn token required
The same status code means different things at different steps. Placing the failure on this row is the first debugging move; everything after that is looking up the cause for that one step.

Why does approving a system user request fail with 403?

Because approval is not a signature, it is a delegation. When your customer clicks approve on the request your system created, Altinn delegates the named access packages to the system user, and the person doing that must hold the authority to delegate each one. The registry roles cover the common cases: a managing director or another key role can delegate most packages. The sensitive packages are the trap, since they have no pre-assigned roles, and only the business's hovedadministrator can hand them out.

The failure is confusing in practice because the person approving is usually senior, and seniority is not the check. The check runs per package, so one package in the request that the approver cannot delegate is enough for a rejection, even when the other four would have gone through. Integrators also observe the delegation check rejecting approvals with an error code rather than a readable message, which turns a rights question into what looks like a technical fault.

The fix is to make the request approvable before it is sent. Ask for the narrowest package set the integration actually needs, check which packages carry pre-assigned registry roles and which are administrator-only, and tell the customer who at their end has to do the approving. When a mixed request keeps failing, split it: a request for the uncontroversial packages that goes through today beats one perfect request that sits rejected.

What do the 500s in this flow actually mean?

Officially, a 500 is a server fault and a 403 is a refusal. In this flow the line is blurrier than the status codes suggest: integrators observe server errors at points where the underlying condition is a refusal, for example when an access package delegation is rejected during approval. Nothing about that is documented behaviour to rely on; it is simply what the flow has been observed to do, which changes how you should debug it.

The practical rule: when a 500 appears at an authorisation-shaped step, exhaust the 403 hypotheses before the outage hypotheses. Re-check the approver's authority, the requested packages and the state of the delegation as if the answer had been an explicit refusal, because a blind retry loop against a refusal-shaped 500 burns time and produces a queue of identical failures. If the authority checks out and the error persists, then treat it as an incident.

Two smaller symptoms belong to the same observed cluster. Listing your vendor requests can misbehave at pagination boundaries, so harden the client against a page that does not arrive rather than assuming the list is complete. And a repeated create for a request that already exists has been observed to answer without the confirmUrl the first response carried, so persist that URL when you first receive it instead of re-deriving it from a repeat call.

The system user failure cluster as symptom, likely cause and fix. Items marked observed describe integration experience rather than documented behaviour.
SymptomLikely causeFix
403 approving the requestThe approver does not hold, or cannot delegate, one of the requested access packages.Have the right person approve, or narrow the package set. Administrator-only packages need the hovedadministrator.
Delegation check rejects the approvalThe per-package authority check fails for at least one named package (observed with opaque error codes).Audit the request package by package against the approver's actual authority; split mixed requests.
500 where a refusal makes sense (observed)A rejected package delegation or similar refusal surfacing with a server-error status.Debug it as a 403 first: verify authority and packages before retrying or declaring an outage.
403 on the API call after approvalThe target API requires the exchanged Altinn token (Altinn Apps and the classic platform APIs) and received a raw Maskinporten token. APIs documented to accept system user tokens directly are unaffected.Exchange the token first and present the Altinn token on app and platform calls.
Vendor request list breaks mid-listing (observed)Pagination boundaries misbehaving on the list endpoint.Treat a missing page as a normal condition: bound retries, and reconcile against your own stored request ids.
confirmUrl missing on a repeat (observed)A repeated create for an existing request answering without the URL the first response carried.Persist confirmUrl from the first response and hand the stored value to the customer.

How do I make this whole error class boring?

Separate the three questions the flow keeps collapsing into one request: has authority been asked for, has a human granted it, and is it active right now. Handled as one step, every failure looks the same from the outside. Handled as three, each failure names its own cause, and the debugging table above shrinks to whichever step you are actually on. That separation is a design choice you can make in your own client regardless of what you integrate through.

It is also exactly how Apier's fullmakt flow is shaped. request_fullmakt starts the process and returns an approval URL to put in front of the customer; the approval itself stays a human decision in Altinn; and check_fullmakt answers whether the delegation is active before your integration acts, with Norwegian fix steps you can show the customer when it is not. A missing grant becomes a state your product displays, not an exception it throws.

The concepts underneath, what a system user is, why the customer owns the grant, and how revocation behaves, are owned by the system user delegation guide, and the token half of the story by the token exchange guide. This page stays what it is: the map from symptom to cause when the flow says no.

Make the first call

The sandbox call answers who may act for a company at all, with no key. The TypeScript sample starts a fullmakt request through the brokered flow, which runs on the mock Altinn adapter until live credentials land, and shows where the delegation URL and the follow-up check live.

# 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
// The fullmakt flow: request, human approval, then check before acting.
const res = await fetch("https://www.apier.no/api/v1/fullmakt/request", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.APIER_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
  },
  body: JSON.stringify({
    agent_principal_id: "22222222-2222-4222-8222-222222222222",
    org_number: "999999999",
    scopes: ["altinn:accessmanagement/authorizedparties.read"],
  }),
});

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();
// Forward data.delegation_url to the customer. Once approved, poll:
//   GET /api/v1/fullmakt/999999999
// and act only when data.overall_status answers full.
console.log(data.status, data.delegation_url);

Frequently asked questions

Who can approve a system user request?
Someone whose own authority covers what they are approving. The approval delegates the requested access packages to your system user, and a person cannot delegate a package they do not hold the right to delegate. Registry roles such as the managing director carry that authority for most packages; the sensitive packages have no pre-assigned roles at all, so only the business's hovedadministrator can approve those.
Why did approval fail even though the approver is the CEO?
Because the check is per package, not per person. An approval that bundles several packages needs the approver to be able to delegate every one of them, so a single sensitive package in the request can sink an approval that would otherwise succeed. Splitting the request, or asking for the narrower package set, is usually the practical fix.
What does a 500 during approval actually mean?
Read it as a rejection first and an outage second. Integrators observe server errors in flows where the underlying condition is an authorisation problem, such as a package delegation being refused. Before retrying on a loop, re-check the approver's authority and the requested packages as if the answer had been 403, because in practice that is often what it stood for.
My token is valid and delegation is active. Why still 403?
Check which token you presented. Altinn Apps and the classic platform APIs evaluate claims the raw Maskinporten token does not carry, including the authentication level added by the exchange endpoint. A valid system user token that never went through the exchange can fail authorisation with every delegation in perfect order. Exchange first, then call.
How does Apier remove this error class?
By making the authority state something you read rather than discover through failures. The fullmakt flow runs request, human approval and verification as separate steps: request_fullmakt starts it, the customer approves in Altinn, and check_fullmakt answers whether the delegation is active before your first real call. Failures then arrive as statuses with fix steps, not as surprise 403s in production.