---
title: "Create an agent principal — the identity a fullmakt is brokered onto"
method: POST
path: "/api/v1/fullmakt/principals"
tags: ["Fullmakt"]
---

# Create an agent principal — the identity a fullmakt is brokered onto

`POST /api/v1/fullmakt/principals`

## Request

- Base URL: `https://www.apier.no`
- URL: `https://www.apier.no/api/v1/fullmakt/principals`
- Auth: HTTP bearer

Fullmakt Rails identity step. Creates an agent principal for the calling consumer: the `agent_principal_id` that POST /api/v1/fullmakt/request binds a systembruker delegation onto and POST /api/v1/fullmakt/revoke retires. A principal is a label the consumer declares for one of its own agents (`display_name`, plus an optional list of the Altinn `scopes` it is intended to carry), anchored to Apier's Altinn vendor system id. It is NOT a verified identity and is never presented as one: it holds no authority of its own, starts `pending` with no systembruker bound, and becomes an actor only once a fullmakt is brokered onto it and the company's signing authority approves that delegation inside Altinn. The write is a LOCAL service-role insert plus an immutable `agent_principal.create` audit_log row — no government call is made. Scope: the assignable `fullmakt:write` scope (the same one the request and revoke operations take), chosen at key creation; the default `read:*` key is refused with 403 SCOPE_INSUFFICIENT. Creation is capped per consumer per hour (429 RATE_LIMITED with Retry-After) on top of the per-key rate limit. Supports Idempotency-Key: a replay with the same key and body returns the stored 201 instead of a second principal. Norwegian term: «agentidentitet» (the code name `agent_principal_id` is unchanged).

## Headers

- `Idempotency-Key` string
- `X-Agent-Vendor` string
- `X-Agent-Model` string
- `X-Agent-Run-Id` string

## Request body

- object
  - `display_name` string, required: Human-readable label for the agent this principal represents (shown back on fullmakt status and receipts). 1–200 characters; stored verbatim. Not an identity claim — Apier verifies nothing about the agent behind it.
  - `scopes` string[]: Optional. The Altinn scopes this principal is INTENDED to carry, recorded on the row and the audit trail as a declared intent. Omitted means no intent is recorded (an empty list is stored) — the scopes actually brokered are the ones sent to POST /api/v1/fullmakt/request. Each entry must contain a non-whitespace character; duplicates are rejected (400 VALIDATION_FAILED).

## Response `201`

Principal created. `data.agent_principal.status` is `pending` and `system_user_id` is null until a fullmakt is brokered onto it. Carries the standard `_meta` trust metadata, the X-Correlation-ID header and `Cache-Control: no-store` (a write endpoint is never cached).

- object
  - `success` true, required
  - `data` object, required
    - `agent_principal` object, required: The agent principal as created. A principal is a consumer-declared label for one of the consumer's own agents, anchored to Apier's Altinn vendor system — NOT a verified identity, and holding no authority of its own until POST /api/v1/fullmakt/request binds a systembruker delegation onto it and the company's signing authority approves that delegation in Altinn. Append-only contract: fields are added, never removed or re-typed.
      - `id` string, uuid, required: The `agent_principal_id` that POST /api/v1/fullmakt/request and POST /api/v1/fullmakt/revoke take. Visible to, and usable by, the creating consumer's own API keys only.
      - `display_name` string, required: The label the consumer declared, echoed verbatim.
      - `status` 'pending' | 'active' | 'suspended' | 'revoked', required: Lifecycle status. Always `pending` on creation: no systembruker is bound yet. A fullmakt request flips it to `active` once a DURABLE binding exists; revocation makes it terminal `revoked`.
      - `scopes` string[], required: The intended Altinn scopes recorded on the principal, exactly as sent (an empty array when `scopes` was omitted). A declared intent, not the delegation: the scopes actually brokered are the ones sent to POST /api/v1/fullmakt/request.
      - `system_user_id` string, nullable, required: The write-once systembruker binding. Always null on creation; set by the first durable fullmakt request and never cleared or repointed afterwards.
      - `created_at` string, date-time, required: RFC 3339 creation time (TIMESTAMPTZ, UTC on the wire).
  - `_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`: Request validation failed (error_code=VALIDATION_FAILED) — malformed JSON body, missing or empty `display_name` (or over 200 characters), a whitespace-only or over-200-character scope, duplicate scopes, more than 100 scopes, or an unknown key (the body schema is strict). `explanation.details[]` names each offending field.
- `401`: Authentication failed — missing, malformed, invalid, or revoked API key.
- `403`: The API key lacks the fullmakt:write scope (error_code=SCOPE_INSUFFICIENT). The default `read:*` wildcard does NOT grant the fullmakt write scope: mint a key with `fullmakt:write` via POST /api/v1/account/keys. Never SCOPE_RESERVED — `fullmakt:write` is an assignable scope, not a reserved prefix.
- `409`: Another request with the same Idempotency-Key is still mid-flight (error_code=IDEMPOTENCY_IN_PROGRESS) — retry after it settles.
- `422`: This Idempotency-Key was previously used with a different request body. Idempotency keys must be unique per unique request.
- `429`: Either the per-key rate limit (the standard RateLimitExceeded contract) or the per-consumer hourly principal-creation cap (error_code=RATE_LIMITED; the cap counts creations across all of the consumer's keys). Both carry `Retry-After` in seconds. The creation cap fails CLOSED: if its counter store is unavailable the create is refused rather than let through unmetered.
- `500`: Unexpected server failure (error_code=SERVER_ERROR) — the principal insert failed, or the stored row failed its schema check. Nothing was created. Internal details are never leaked; the raw error goes to Sentry with the correlation id. Retry-safe.
- `503`: Either the Altinn vendor system id is not configured in this environment (error_code=ALTINN_NOT_CONFIGURED — an operator-side gap, not a caller error; nothing was created), or the idempotency reservation service is temporarily unavailable (error_code=UPSTREAM_UNAVAILABLE) — clients using Idempotency-Key should retry with the same key after a short backoff.

---

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