How can AI agents safely access Norwegian government services?
Through delegated, scoped authority and structured JSON, never by putting a government credential inside the model. The agent holds an API key that grants named read scopes; the enterprise certificate and the Maskinporten token exchange stay behind the API. Acting for a specific company additionally requires that company to have delegated authority to the calling system through Altinn, which is a decision only the customer can make. Every call carries a correlation id, so what the agent did stays reconstructable afterwards.
How does an AI agent authenticate with Altinn?
Indirectly, and that is the whole design. Altinn 3 does not issue API keys. It expects a Maskinporten access token, and the key behind that token is anchored on an enterprise certificate (virksomhetssertifikat) issued to a Brønnøysund-registered entity. Handing that certificate to an agent is a poor idea for reasons that have nothing to do with the model being untrustworthy: prompts and tool arguments get logged, replayed, cached and sometimes shown to end users, and a long-lived organisational identity should not travel through any of those.
So the agent authenticates against Apier instead, with a single Authorization: Bearer key, and Apier holds the certificate, signs the JWT assertions and rotates the tokens on the other side of the call. The agent never sees a government token and has no code path that could leak one. The full mechanics of that exchange are covered in the guide on authenticating against Norwegian government APIs.
The second leg is authority rather than identity. Proving which organisation is calling says nothing about whether that organisation may act for the company in question. That permission comes from an Altinn System User delegation, granted by the customer to your registered system. No amount of infrastructure removes it, because it encodes a decision only the customer is entitled to make, and an agent that treats a missing delegation as a retryable error will loop forever against a wall that is working correctly.
What stops an agent acting beyond its delegated authority?
Two limits that fail independently, which matters more than either limit being strict. The first is the key itself. Every API key carries an explicit scope array in domain:action form, and every protected endpoint declares the scope it requires, checked before any handler logic runs. The assignable vocabulary is read-side and narrow: read:brreg, read:altinn, read:changes, subscribe:webhooks and a small number of siblings. Wildcards widen the action segment only, so read:* grants any read and nothing else.
The scope prefixes that would authorise a binding government write are reserved and cannot be issued at all. A key holding one grants nothing, and the endpoints that would consume one refuse the call outright rather than falling through to a permissive default. That is a deliberate ceiling, not a gap waiting to be filled: it means no key in circulation today can be talked into performing a binding submission, whatever an agent is persuaded to attempt.
The second limit is the customer's delegation, and no scope on your side can substitute for it. The practical advice for agent authors is to resolve authority before planning an action rather than discovering it in a failure: ask /api/v1/company/{org}/authority who can sign and how, and branch on the answer. A joint classification means two signatures are required and the correct agent behaviour is to hand back to a human, not to retry.
How is an agent's action made auditable afterwards?
By making one identifier follow the request through every layer that touches it. Each call carries an X-Correlation-ID: send a valid UUID v4 and it is reused verbatim, send nothing and one is generated. That id is echoed on the response headers and persisted on the audit row, the rule-evaluation record and the compliance state transition produced by the same call, so a single query rebuilds everything one agent action caused.
Responses are also self-describing about their own provenance. Every response carries a _meta block with a response_timestamp and a response_hash, a SHA-256 over the canonicalised body, and lands a matching row in the provenance log. That combination answers a question logs alone cannot: not just what your system received, but that the bytes it acted on are the bytes that were served. For an autonomous caller this is the difference between an audit trail and an assertion.
Build the agent against the sandbox before any of this matters in production. The sandbox never makes a network call to a government upstream on any verb, so an agent that misbehaves there cannot reach Altinn at all, and ?simulate_error= lets you rehearse the missing-delegation and invalid-token paths deliberately rather than waiting to meet them. Every sandbox response is marked _meta.is_sandbox: true, so the agent can tell a simulated result from a real one without guessing.
Make the first call
The sandbox request below needs no credential at all. The TypeScript snippet is the production equivalent, resolving signing authority before an agent commits to a plan.
# Zero-auth sandbox: rehearse the agent's read path with no credential.
curl -s https://www.apier.no/api/v1/sandbox/public/company/999999999/authority// Check authority BEFORE planning an action, not after it fails.
const res = await fetch(
"https://www.apier.no/api/v1/company/999999999/authority",
{
headers: {
Authorization: `Bearer ${process.env.APIER_API_KEY}`,
"X-Correlation-ID": crypto.randomUUID(),
},
},
);
if (!res.ok) {
// An error envelope carries no `data`, so read the code rather than
// dereferencing undefined and losing the reason the check failed.
const { error_code, explanation } = await res.json();
throw new Error(`${error_code}: ${explanation.summary}`);
}
const { data } = await res.json();
// "sole" | "joint" | "by_role" | "prokura_only" | "no_authority" | "unknown"
if (data.classification !== "sole") {
throw new Error("More than one signature required. Hand back to a human.");
}Frequently asked questions
- Should an AI agent hold Maskinporten credentials directly?
- No. An enterprise certificate is a long-lived organisational identity, and a model context is the wrong place for one: prompts get logged, replayed and sometimes shown to users. The safer shape is for the agent to hold a scoped API key that grants specific read rights, while the certificate and the token exchange stay behind the API on infrastructure the agent cannot reach.
- What stops an agent doing more than it was authorised to do?
- Two independent limits. The key carries an explicit scope list in domain:action form, and every protected endpoint declares the scope it requires, so a key without that scope is refused before any handler logic runs. Separately, acting for a company requires that company to have delegated authority to the calling system, which no scope on your side can grant.
- Can an agent submit a binding filing to Altinn today?
- Not through this API. The scope prefixes that would authorise a binding write are reserved and cannot be assigned to a key, and binding submission runs against sandbox fixtures rather than a real Altinn endpoint. Reads, authority resolution and dry-run payload validation are live, so an agent can prepare and check a filing without being able to send one.
- How do I prove afterwards what an agent actually did?
- Every request carries an X-Correlation-ID, reused verbatim when you supply a valid UUID v4 and generated when you do not. That id is persisted on the audit trail and on the rule-evaluation record for the same call, so one query reconstructs the full slice of history a single agent action produced.
- Is the sandbox safe to point an autonomous agent at?
- Yes, and that is its purpose. The sandbox path never makes a network call to Altinn, Maskinporten, Brønnøysund, Skatteetaten or NAV on any verb, including writes. A sandbox filing is a local state transition, so an agent looping on that surface cannot reach a government system no matter what it decides to do.