---
title: "Redeem an owner-issued issuance token for an API key"
method: POST
path: "/api/v1/account/issuance-tokens/redeem"
tags: ["Account"]
---

# Redeem an owner-issued issuance token for an API key

`POST /api/v1/account/issuance-tokens/redeem`

## Request

- Base URL: `https://www.apier.no`
- URL: `https://www.apier.no/api/v1/account/issuance-tokens/redeem`
- Auth: none required

Agent-side half of headless key issuance — the first Apier endpoint a cold agent can call that ends with it holding its own API key, with no browser and no inbox. Zero-auth by definition: the presented one-time token IS the credential, and it only exists because the human account owner minted it in the dashboard — agents cannot self-issue. Redemption is strictly single-use and ATOMIC: one SECURITY DEFINER transaction consumes the token (compare-and-swap UPDATE … RETURNING), mints the key through the same canonical create_consumer_key RPC the dashboard uses (scopes ["read:*"], the account's existing max-3-active-keys cap enforced under its advisory lock), and backlinks the issued key onto the token row. Two concurrent redemptions of the same token therefore yield exactly one key. If the account already holds 3 active keys the whole transaction rolls back — the token is NOT consumed (409 MAX_KEYS_REACHED tells the agent to have the owner free a key slot, then retry with the SAME token). Every other failure — expired, already-used, revoked, or never-existed — returns ONE uniform 401 ISSUANCE_TOKEN_INVALID body that is byte-identical across all four cases: a probe learns nothing about why a guess failed, only that it failed. plaintext_key is returned EXACTLY ONCE in the 201 body (same shape as POST /api/v1/account/keys); the accepted terms version in force is stamped into the redemption audit event. Rate limits are three fail-safe layers on hourly windows: 5 attempts per hour per (IP, token) — keyed on the token's hash as well as the IP so a clean client's first attempt with a distinct token is never blocked by unrelated traffic on a shared egress IP — plus a per-IP wall of 30 attempts per hour across all tokens, both fail-CLOSED, plus a global hourly ceiling across all callers, fail-OPEN. A definitive over-limit returns 429 RATE_LIMITED with the store's actual remaining window in Retry-After; a rate-limit-store failure returns an honest retryable 503 RATE_LIMIT_CHECK_UNAVAILABLE (Retry-After: 30) instead of a fake limit verdict — nothing is redeemed or counted in either case.

## Request body

- object
  - `token` string, required: The plaintext apr_issue_… token exactly as the account owner handed it over. Sent in the body (never a URL parameter); hashed server-side immediately and never stored, logged, or echoed.

## Response `201`

Token redeemed — the agent now holds its own key. plaintext_key is the ONLY copy it will ever see (same one-time contract as POST /api/v1/account/keys). Carries the full no-cache directive set so no intermediary ever caches the plaintext body.

- object
  - `success` true, required
  - `data` object, required
    - `id` string, uuid, required
    - `name` string, required: Server-fixed label ('Agent key (token-issued)') — the redeem body deliberately carries no name field; every extra caller-controlled field on a credential path is attack surface.
    - `scopes` string[], required
    - `created_at` string, date-time, required
    - `plaintext_key` string, required: ONLY returned here. Never persisted. Never reissued.
  - `_meta` TrustMetadata, required: Metadata included in every response for trust and verifiability.
    - `rulebook_version` string, required: Current Rulebook version. Internal rule-version records carry structured origin metadata (`initial_seed` | `human_review` | `lovdata_drift_response` | `audit_finding` | `customer_question` | `legal_correction`) for operator forensics, but that attribution is deliberately NOT exposed on public responses to avoid leaking operational signal.
    - `data_freshness` string, date-time, required: When this data was last fetched (ISO 8601 UTC).
    - `last_verified` string, date-time, required: When this data was last verified against source (ISO 8601 UTC).
    - `source` string, required: Origin of this data. Defaults to `apier.no`. The agent-readiness probe (`GET /api/v1/health/agent-readiness`) is the one surface that may instead emit `apier.no telemetry` — it does so when at least one of its endpoint rates came from measured live telemetry, and falls back to `apier.no` when every rate is a static default; all other endpoints always emit `apier.no`.
    - `data_source` string: Authoritative origin of the underlying data (listed alongside rulebook_version, data_freshness, last_verified, legal_basis for Rulebook-influenced endpoints). Distinct from `source` (the response origin): `data_source` identifies the authoritative data publisher — e.g. 'Brønnøysund Enhetsregisteret + Apier Universal Rulebook'. Present on registry-backed endpoints.
    - `served_from` 'cache' | 'live' | 'static': Cache-aware endpoints only — 'cache' when the response was served from the local copy, 'live' when the gateway just refreshed from the upstream, 'static' when the payload was computed deterministically from pure code with no DB or upstream read (e.g. /v1/public/deadlines). Absent on endpoints that don't carry this semantic.
    - `stale` boolean: Cache-aware endpoints only — true when the upstream was unreachable and a within-limit cached row was served as a fallback. Absent on normal responses.
    - `schema_version` string, required: OpenAPI contract version at the time of the response; matches `info.version`. Always present on every `/v1/*` response so a future schema break can be traced to the exact rows / audit events that predate / postdate it. Agent-facing machine interfaces (v1/capabilities, workflows.json) may override with their own narrower surface version; all other endpoints inherit the manifest-level version.
    - `legal_basis` string: Legal basis under which the response data is reused. Present on Rulebook-influenced endpoints (v1/company/{org}/obligations, /v1/company/{org}/deadlines). Common value: 'NLOD — public registry reuse'.
    - `rule_count` integer: Rule-evaluation count — total rules evaluated for this request. Present on endpoints that invoke the Rulebook evaluator (v1/company/{org}/obligations).
    - `applicable_count` integer: Count of rules whose evaluation_result is 'applicable'. Present on evaluator endpoints.
    - `not_applicable_count` integer: Count of rules whose evaluation_result is 'not_applicable'. Present on evaluator endpoints.
    - `insufficient_data_count` integer: Count of rules whose evaluation_result is 'insufficient_data' (Tier 2 gated). Present on evaluator endpoints. rule_count = applicable_count + not_applicable_count + insufficient_data_count.
    - `cache_age_ms` integer, nullable: Cache layer: milliseconds since the cached row was computed, or null on a cache miss (served_from === 'live'). Agents use it alongside data_freshness as a staleness signal. Present on endpoints wired to the cache (v1/company/{org}/obligations, /deadlines, /summary, /v1/public/obligations, /v1/public/deadlines).
    - `response_time_ms` integer: Speed signal: total server-side processing time in milliseconds, measured via performance.now() at the route boundary. Mirrored on the `X-Apier-Response-Time-Ms` header so agents can key on it without parsing the body. Present on every endpoint.
    - `response_timestamp` string, date-time: RFC 3339 / ISO 8601 UTC timestamp stamped by the response builder at wire-exit. Mirrored on the `provenance_log.response_timestamp` column by a fire-and-forget writer. Distinct from `data_freshness` (when upstream data was fetched) and `last_verified` (when the citation was verified); this is the moment the envelope left the server. Always populated at runtime — deliberately left out of the `required` list to avoid a cascading contract change that would invalidate every pre-existing example envelope in this spec (append-only response contracts). A future revision may promote it into the `required` list alongside a bulk example refresh.
    - `response_hash` string: SHA-256 hash of the canonical-JSON form of the envelope with `_meta` stripped before hashing, prefixed with `sha256:`. Mirrored on `provenance_log.response_hash` so a consumer can independently re-hash the bytes they received and compare against what we persisted (integrity receipt). Stripping `_meta` is load-bearing: volatile fields (response_time_ms, cache_age_ms, served_from) and the hash itself would otherwise feed into the digest and break the cache-hit-bytewise-identical invariant.
    - `source_snapshot_id` string: Optional free-text pointer into an upstream sync snapshot the response derives from — e.g. a Brreg sync's `brreg-<ISO>` snapshot id. Absent when the response is not tied to a specific upstream snapshot (capabilities, tools, static deadlines).
    - `is_sandbox` true: Present and `true` only when the response was generated from sandbox fixture data (currently `ALTINN_MODE=sandbox` on `/api/v1/altinn/list-acting-capacity`). Absent (NOT `false`) on every other response — the absent-vs-false distinction is deliberate so downstream agents branching on `if (response._meta.is_sandbox)` cannot be misled into treating non-sandbox responses as positively-confirmed-not-sandbox. Distinct from the `/v1/sandbox/*` `is_sandbox: true` literal in `SandboxMeta` (which carries the sandbox surface tag); this flag rides through the production `/api/v1/altinn/*` surface to forensically tag a sandbox-mode response that an OEM evaluator received.
    - `cost_nok` number: What this call debited from the caller's prepaid credit balance, in NOK, rendered from the integer-øre ledger (50 øre → 0.5; the ledger itself never leaves whole øre and no float arithmetic touches a balance). Present on the 2xx body of every metered company-data read — the same debit that sets the `X-Credits-Cost-Ore` header, so the two never disagree. `0` when nothing was charged: credit enforcement running dark (the response then also carries `X-Credit-Check: shadow`) or the sandbox surface. Absent on unmetered endpoints and on error envelopes — a refunded 5xx/403/404 claims no cost rather than a wrong one. Lives under `_meta`, which `response_hash` excludes, so the provenance receipt is unaffected; the route's ETag is computed before the credit wrapper stamps it, so a conditional request can still short-circuit to 304. Append-only addition.
    - `wallet_remaining_nok` number, nullable: The prepaid credit balance remaining AFTER this call's debit, in NOK (same rendering as `cost_nok`; mirrors the `X-Credits-Balance-Ore` header). `null` when no balance was read: credit enforcement running dark, or the sandbox surface. Present wherever `cost_nok` is. An agent planning a sequence of metered calls can divide this by the per-call price from GET /api/v1/pricing to know how many reads remain before a 402 INSUFFICIENT_CREDITS. Append-only addition.
    - `spend_cap_remaining_nok` number: What the calling key may still spend under its tighter spend cap (Europe/Oslo calendar day or month) AFTER this call's debit, in NOK (same rendering as `cost_nok`; never below 0). Present on the 2xx body of a metered read only when credit enforcement is live AND the key has a daily or monthly cap (set with PATCH /api/v1/account/keys/{id}); absent otherwise. When it reaches 0 the next metered call on this key returns 402 SPEND_CAP_REACHED until the cap resets. Append-only addition.

## Other responses

- `400`: VALIDATION_FAILED — malformed JSON or a body that is not exactly { token }.
- `401`: ISSUANCE_TOKEN_INVALID — the uniform failure. Expired, already-used, revoked, and never-existed tokens all return this byte-identical body; the distinction is deliberately undisclosed.
- `409`: MAX_KEYS_REACHED — the account already holds 3 active keys, so no key was issued AND the token was NOT consumed (the atomic transaction rolled back). Have the owner revoke a key, then retry with the same token.
- `429`: RATE_LIMITED — a definitive over-limit on one of the redemption limiter layers (5/hour per IP+token, 30/hour per IP, or the global hourly ceiling). Retry-After carries the store's actual remaining seconds in the current window — wait it out, then retry. Never returned for a rate-limit-store failure (that is the 503 below).
- `500`: Unexpected DB error.
- `503`: RATE_LIMIT_CHECK_UNAVAILABLE — the rate-limit store could not be consulted, so this credential surface refused fail-closed. No redemption work ran and the issuance token was NOT consumed; the response is retryable with a short Retry-After (30 seconds). Replaces the previous behaviour of disguising a store outage as a 429 rate-limit verdict on a client's first attempt.

---

[API](https://skmtc.dev/apier/apis/apier-no-api.md) · [All operations](https://skmtc.dev/apier/apis/apier-no-api/llms.txt) · [OpenAPI document](https://skmtc.dev/apier/apis/apier-no-api/revisions/9ec19bc87311?raw)
