Authentication
Apier's consumer auth model: API keys, tiers, per-endpoint access categories, and the sandbox isolation contract.
[Cite this as: Apier.no Docs v0.1.0 — last updated 2026-08-22]
Apier uses bearer API keys for authentication. Every key is SHA-256 hashed at rest and is never logged. Keys are issued in the dashboard and shown in full exactly once; store yours securely.
Getting an account and your first key
Creating the account always involves a person: someone opens one emailed link, once, to bring the account into being. (Once the account exists, its owner can issue keys to headless agents without a browser; see Headless key issuance below.) The flow:
- Sign up.
POST /api/v1/account/signupwith a JSON body{ "email": "...", "company_name": "...", "consent": true }. All three fields are required to sign up; sendingemailalone is instead treated as a returning-user login. A200with{ "success": true, "data": { "ok": true } }means the request was accepted. - A human opens the magic link. The signup call issues no API key. It emails a magic link to the address you supplied; someone with access to that inbox must open it. The account, and its first key, are created on that first sign-in, not by the signup call.
- The first key is shown once. On that first sign-in Apier provisions a free-tier key (
apr_free_…) and shows it in full exactly once. From then on you manage your keys (up to three) from your dashboard.
Before signing up you can read pricing with no key (GET /api/v1/pricing), review the terms, and exercise the documented public-sandbox endpoints against org 999999999. Issuing further keys with POST /api/v1/account/keys also needs an authenticated browser session, not a bearer token; see Key rotation below. The one headless path is the owner-issued issuance token, next.
Headless key issuance (owner-issued tokens)
An agent can obtain its own API key with no browser and no inbox, but only because a human chose to let it. The account owner mints a one-time issuance token in the dashboard and hands it to the agent out of band; the agent redeems it, once, for a read:*-scoped key. Agents cannot self-issue: there is no endpoint that turns "I am an agent" into a credential; only the owner-minted token does, and the owner can revoke it any time before redemption.
The full path from cold agent to first authenticated call:
-
Sign up and sign in once (the human steps above); the account must exist.
-
Owner mints a token. In the dashboard, under Agent issuance tokens, mint a token (
apr_issue_…). It is shown in full exactly once, is valid for 24 hours, is strictly single-use, and at most 3 unredeemed tokens can be outstanding at a time. Only its SHA-256 hash is stored. -
Owner hands the token to the agent through whatever secret channel already exists (environment variable, secrets manager, MCP client config).
-
Agent redeems it:
curl -X POST https://www.apier.no/api/v1/account/issuance-tokens/redeem \ -H "content-type: application/json" \ -d '{"token":"apr_issue_<your_token_here>"}'The
201body returns the agent's own key (plaintext_key, scopes["read:*"]) exactly once, the same one-time contract as every other key mint. The redemption is atomic and single-use: a second redemption of the same token fails, and two concurrent redemptions issue exactly one key. An MCP-pure agent redeems the same token without leaving the protocol via the keylessredeem_issuance_tokentool (same route, same semantics, same one-time contract). Minting and revoking tokens remain dashboard-only by design; there is no mint tool on any agent surface. -
First authenticated call with the returned key, e.g.
GET /api/v1/company/999999999/summary.
Failure behaviour is deliberately uniform: an expired, already-used, revoked, or never-existed token all return the same 401 ISSUANCE_TOKEN_INVALID body; the only fix is a fresh token from the owner. One exception is disclosed: if the account already holds 3 active keys, redemption returns 409 MAX_KEYS_REACHED and the token is not consumed, so the agent can retry with the same token after the owner frees a key slot. Redemptions are rate-limited per IP, and every mint, revoke, and redemption lands in the account's audit trail (the redemption event records the issued key id and the terms version in force).
Making an authenticated request
Pass your key in the Authorization header:
GET /api/v1/company/{org}/summary HTTP/1.1
Authorization: Bearer apr_free_<your_key_here>The MCP server and the quickstart examples set this header for you.
Key formats
One prefix scheme runs across the whole API, so you can tell at a glance what a token is and where it works:
| Prefix | What it is | Where it works |
|---|---|---|
apr_free_… | A free-tier key, issued by default on first sign-in | Every endpoint; Category B at the Free-tier rate limit |
apr_test_… | An issued test-tier key | Every endpoint, against synthetic data |
apr_live_… | An issued production key | Production /api/v1/* — real filings |
apr_issue_… | A one-time key-issuance token, minted by the account owner in the dashboard | Only POST /api/v1/account/issuance-tokens/redeem — it is not a bearer key and authenticates nothing else |
apier_sandbox_test_<suffix> | The no-signup synthetic sandbox bearer | The /api/v1/sandbox/* routes only |
Issued keys (apr_…) come from your dashboard and are shown in full exactly once. The synthetic sandbox bearer needs no signup; the <suffix> (1–64 characters of [A-Za-z0-9_-], typically a crypto.randomUUID()) is a private session namespace you choose. See the quick start for the sandbox walkthrough.
Access categories
Apier endpoints fall into two categories:
| Category | Auth required | Examples |
|---|---|---|
| Category A | No — open to any caller | /api/v1/public/obligations, /api/v1/public/deadlines, /api/v1/health/agent-readiness |
| Category B | Yes — valid API key | /api/v1/company/{org}/summary, /api/v1/company/{org}/obligations, /api/v1/actions/execute |
Category A endpoints are safe to call from a browser or an unauthenticated agent to discover obligations and deadlines without exposing a key.
Keyless MCP tools
Six public tools on the MCP server are callable via tools/call without an API key: five wrap zero-auth read endpoints, and one wraps the zero-auth issuance-token redemption route:
get_public_obligationsget_public_deadlinesexplain_compliance_errorget_exchange_rateget_pricingredeem_issuance_token: converts a one-time issuance token the account owner minted in the dashboard into the agent's ownread:*-scoped key; keyless by necessity, since the caller cannot hold a key yet
Keyless tools/call requests are rate-limited per IP (100 requests / hour). Every other MCP tool (company data, acting capacity, authorization, and actions) requires a key with the matching scope and returns 401 without one. (The one non-key credential the MCP surface accepts is the synthetic apier_sandbox_test_<suffix> bearer described under the sandbox isolation contract below; it authenticates sandbox-routed tools only, never the production surface.)
"Without an API key" means no Authorization header at all. If you do send a header but the key is invalid or revoked, the call returns 401; it is not silently downgraded to the keyless path, even for one of the keyless public tools.
OAuth 2.1 sign-in for consumer hosts
Consumer hosts such as the Claude.ai and ChatGPT web connectors cannot set an Authorization header themselves, so the keyed MCP tools are unreachable from them with an API key alone. The MCP server is a spec-compliant OAuth 2.1 resource server for that case: it verifies access tokens minted by an external authorization server and never issues tokens itself. The authorization server is the project's own identity provider, with Dynamic Client Registration and a consent page at /oauth/consent where the signed-in account owner approves or denies each connecting app. OAuth sign-in is enabled in production (switched on 2026-08-25), and the whole flow is self-service: signing in and approving the consent page is all it takes. This is what an agent sees:
- A
tools/callon a keyed tool with no credential, or with a token the server rejects, returns401with a challenge such asWWW-Authenticate: Bearer realm="apier-mcp", error="invalid_token", error_description="...", resource_metadata="https://www.apier.no/.well-known/oauth-protected-resource/api/mcp". GET /.well-known/oauth-protected-resource/api/mcp(the RFC 9728 §3.1 path-qualified URL for the/api/mcpresource; a client can derive it from the resource identifier alone, and the host-level/.well-known/oauth-protected-resourceserves the same document as an alias) returns the RFC 9728 document:resource(the audience a token must carry;https://www.apier.no/api/mcpis the production default and operators can override it per deployment),authorization_servers(the one trusted issuer, derived from the project's identity provider by default and likewise operator-overridable; the served document is always the authoritative value),bearer_methods_supported: ["header"]andscopes_supported. That last field lists ONLY the identity scopes the authorization server itself can issue (openid,profile,email,phone,offline_access); Apier's own permission scopes are deliberately absent, because a scope the paired authorization server cannot mint would make the authorize request fail outright. The host obtains a token from that issuer for thatresource.- The host retries with
Authorization: Bearer <jwt>. The server verifies the signature against the issuer's JWKS with a strict asymmetric-only allowlist (ES256 or RS256;noneand every HMAC algorithm are rejected; fetched and cached; refreshed once on an unknownkid; fail closed if the key set is unreachable), theiss, theaud, andexp/nbf, then maps the token's(issuer, sub)to one dedicated Apier API key. From that point the call carries that key's scopes, tier, rate limits and audit attribution exactly as if the key had been presented. A token whose identity has no live mapping is rejected withOAUTH_SUBJECT_UNMAPPED; reconnecting through the consent page repairs it.
The mapping is created self-service at consent time. The moment the signed-in account owner clicks Allow on /oauth/consent, Apier provisions the connection in one atomic step: it resolves (or creates) the account, mints a dedicated API key labeled MCP OAuth connection, and links the OAuth identity to that key. No operator is involved, and no key needs to exist beforehand. The dedicated key is scoped to exactly read:brreg, read:altinn, read:digdir, read:norgesbank and read:changes (never read:*), its plaintext never exists (it is usable only through the OAuth mapping, so it can never leak as a Bearer credential), and it counts toward the account's 3-active-keys cap. If the account already holds 3 active keys, the consent page says so and asks the owner to revoke one in the dashboard first; nothing is approved until that succeeds. Approving the same connection again reuses the existing mapping instead of minting another key.
Revoking is self-service too. The dashboard's Connected AI assistants panel shows each connection (the issuer and connect date, plus the dedicated key's label and scope list) with a Disconnect action. Disconnecting revokes the mapping and its dedicated key together in one transaction, so the connected assistant loses access immediately; a later reconnect through the consent page mints a brand-new mapping and key.
OAuth scopes cover identity only. The scopes in the authorization request establish who is signing in; they say nothing about what the connected agent may do. Every Apier permission comes from the API key the token subject is mapped to, and that key's scopes, tier and rate limits decide each call. Requesting a wider OAuth scope grants no extra Apier access, which is exactly why no Apier permission scope (read:brreg, read:altinn, subscribe:webhooks and the rest) appears in scopes_supported.
The six keyless tools stay keyless either way, and an Apier API key in the same header always works regardless of OAuth. The rejection codes you may meet (OAUTH_TOKEN_INVALID, OAUTH_TOKEN_EXPIRED, OAUTH_AUDIENCE_MISMATCH, OAUTH_SUBJECT_UNMAPPED, OAUTH_JWKS_UNAVAILABLE) are all in the error registry; only the JWKS outage is worth retrying with the same token. The surface stays flag-gated in code and is fail-safe off by default, so on a deployment where an operator has not switched it on the metadata document answers 404 OAUTH_DISABLED, the 401 challenge is the plain Bearer realm="apier-mcp", error="invalid_token", and a JWT-shaped bearer is treated like any other unknown key (AUTH_INVALID_KEY).
The sandbox and its isolation contract
The sandbox surface (/api/v1/sandbox/...) returns deterministic synthetic data and never calls a government system. It accepts the no-signup apier_sandbox_test_<suffix> bearer (and a real key too), and the boundary between sandbox and production is enforced both ways:
- A real key used on
/api/v1/sandbox/*is accepted, but the response is still mock; a sandbox request never reaches a government upstream, so nothing is ever submitted there. - The synthetic bearer is rejected on production
/api/v1/*routes with401 AUTH_INVALID_KEY; it works only on the sandbox surface.
So a misrouted call fails safe in both directions: a production key cannot accidentally file through the sandbox, and a sandbox bearer cannot reach the real API. See Going live for the switch to real filings.
Rate limits
Company-data (Category B) limits scale with your tier:
| Tier | Category B limit |
|---|---|
| Free | 30 requests / min |
| Starter | 150 requests / min |
| Professional | 300 requests / min |
| Enterprise | Unlimited |
Category A endpoints share a separate public rate limit (1 000 requests / min per IP) and are not counted against your tier quota.
Scopes
Each key carries a scopes array (default ["read:*"]). Category B endpoints declare the scope they require; for example, /api/v1/actions/execute requires read:actions. Reserved prefixes (write:, act:, delegate:) are not assignable today. The full assignable set is listed on /api/v1/capabilities.
Key rotation
You may hold up to 3 active keys at once. That overlap capacity is what makes rotation zero-downtime: a new key and the key it replaces are both valid at the same time, so there is never a window where every credential is dead.
The MCP server authenticates with the same bearer key as the REST API (it reads it from the Authorization: Bearer header), so rotating an MCP consumer's key is the same three-step flow, applied to whatever holds that header (an environment variable, a secrets manager, your agent's MCP client config):
- Mint the new key.
POST /api/v1/account/keysreturns the plaintext key exactly once in the response body; store it immediately. (The dashboard's "Create key" button does the same thing.) You now hold both the old and the new key. - Cut over, then verify. Point the
Authorization: Bearerheader at the new key and confirm a real call succeeds: for MCP, atools/callthat returns a result; for REST, any Category B request that returns200. Because the old key is still live, a failed cutover is recoverable: roll the header back and retry. - Revoke the old key.
DELETE /api/v1/account/keys/{id}(or "Revoke" in the dashboard) takes effect immediately. From that moment the old key is rejected on every surface: each MCPtools/calland each REST request made with it returns401 AUTH_INVALID_KEY(the same unified failure shape a never-issued key returns, so a revoked key discloses nothing about its former validity). The new key keeps working uninterrupted.
Revocations and rejected calls are recorded in your account's audit trail, so a rotation (and any lingering client still presenting the retired key) is auditable after the fact.
There is no single "rotate" endpoint: rotation is deliberately the two primitives above (POST to mint, DELETE to revoke) so the overlap window (and the verification step inside it) stays under your control. If you already hold 3 active keys, revoke one before minting the replacement.
How keys are enforced
All authentication logic lives in src/middleware/auth.ts and src/lib/auth/*. On every Category B request the key is resolved, validated against the SHA-256 hash stored in Supabase, scope-checked, and rate-limited before the handler runs.