Skip to content
Apier.no

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:

  1. Sign up. POST /api/v1/account/signup with a JSON body { "email": "...", "company_name": "...", "consent": true }. All three fields are required to sign up; sending email alone is instead treated as a returning-user login. A 200 with { "success": true, "data": { "ok": true } } means the request was accepted.
  2. 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.
  3. 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:

  1. Sign up and sign in once (the human steps above); the account must exist.

  2. 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.

  3. Owner hands the token to the agent through whatever secret channel already exists (environment variable, secrets manager, MCP client config).

  4. 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 201 body 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 keyless redeem_issuance_token tool (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.

  5. 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:

PrefixWhat it isWhere it works
apr_free_…A free-tier key, issued by default on first sign-inEvery endpoint; Category B at the Free-tier rate limit
apr_test_…An issued test-tier keyEvery endpoint, against synthetic data
apr_live_…An issued production keyProduction /api/v1/* — real filings
apr_issue_…A one-time key-issuance token, minted by the account owner in the dashboardOnly 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 bearerThe /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:

CategoryAuth requiredExamples
Category ANo — open to any caller/api/v1/public/obligations, /api/v1/public/deadlines, /api/v1/health/agent-readiness
Category BYes — 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_obligations
  • get_public_deadlines
  • explain_compliance_error
  • get_exchange_rate
  • get_pricing
  • redeem_issuance_token: converts a one-time issuance token the account owner minted in the dashboard into the agent's own read:*-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:

  1. A tools/call on a keyed tool with no credential, or with a token the server rejects, returns 401 with a challenge such as WWW-Authenticate: Bearer realm="apier-mcp", error="invalid_token", error_description="...", resource_metadata="https://www.apier.no/.well-known/oauth-protected-resource/api/mcp".
  2. GET /.well-known/oauth-protected-resource/api/mcp (the RFC 9728 §3.1 path-qualified URL for the /api/mcp resource; a client can derive it from the resource identifier alone, and the host-level /.well-known/oauth-protected-resource serves the same document as an alias) returns the RFC 9728 document: resource (the audience a token must carry; https://www.apier.no/api/mcp is 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"] and scopes_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 that resource.
  3. 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; none and every HMAC algorithm are rejected; fetched and cached; refreshed once on an unknown kid; fail closed if the key set is unreachable), the iss, the aud, and exp / 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 with OAUTH_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 with 401 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:

TierCategory B limit
Free30 requests / min
Starter150 requests / min
Professional300 requests / min
EnterpriseUnlimited

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):

  1. Mint the new key. POST /api/v1/account/keys returns 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.
  2. Cut over, then verify. Point the Authorization: Bearer header at the new key and confirm a real call succeeds: for MCP, a tools/call that returns a result; for REST, any Category B request that returns 200. Because the old key is still live, a failed cutover is recoverable: roll the header back and retry.
  3. 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 MCP tools/call and each REST request made with it returns 401 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.

Building with an LLM? Read llms.txt for agent-oriented integration guidance.