---
title: "Broker a fullmakt: bind an agent principal to an Altinn systembruker delegation"
method: POST
path: "/api/v1/fullmakt/request"
tags: ["Fullmakt"]
---

# Broker a fullmakt: bind an agent principal to an Altinn systembruker delegation

`POST /api/v1/fullmakt/request`

## Request

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

Fullmakt Rails core flow. Given an existing agent principal (created with POST /api/v1/fullmakt/principals — same `fullmakt:write` scope), a customer org_number, and a scope set, Apier brokers an Altinn systembruker delegation and binds the returned system_user_id back onto the principal. Ordering contract: Altinn is called FIRST; only on upstream success is the delegation row persisted, then the principal binding written, then a 'pending' principal flipped to 'active'. Principal activation always reflects a DURABLE binding — if a local follow-up write degrades after the upstream call succeeded, the response is still 201 (the brokered delegation and its approval URL must not be lost) and the degradation is surfaced as a machine-readable token in `data.warnings` so the caller can reconcile instead of blind-retrying (a blind retry would broker a SECOND delegation). The principal binding is write-once: a principal already bound to a different systembruker keeps its binding and the response carries `principal_already_bound_mismatch`. The delegation itself starts as 'pending' until the company's signing authority approves the `delegation_url`. The integration runs against the mock Altinn adapter until live partner validation clears; the response shape is identical in both modes. Recorded in the append-only audit log as `fullmakt.request`.

## Headers

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

## Request body

- object
  - `agent_principal_id` string, uuid, required: The agent principal this delegation binds to. Must be owned by the calling consumer — an unknown id and another consumer's id both return the identical 404 FULLMAKT_PRINCIPAL_NOT_FOUND (no cross-consumer existence leak). Only principals in status 'pending' or 'active' may request; 'revoked' and 'suspended' return 409 FULLMAKT_PRINCIPAL_NOT_ELIGIBLE.
  - `org_number` string, required: Norwegian organisation number of the customer company delegating authority. Exactly 9 digits, matching Brønnøysund Register Centre format. Any other shape returns 400 VALIDATION_FAILED before the upstream call is made.
  - `scopes` string[], required: Altinn scopes to request for the systembruker (e.g. 'altinn:accessmanagement/authorizedparties.read'). At least one scope is required. The scope list appears verbatim on the approval page shown to the company's signing authority, so request only what the agent actually needs.
  - `validity_days` integer: Desired delegation validity window in days. Optional — if omitted, Altinn applies its own default cap. Upper bound of 10 years matches Altinn's longest supported delegation term.
  - `label` string: Consumer-supplied label shown in the dashboard to distinguish multiple delegations. Never forwarded to Altinn.

## Response `201`

Delegation brokered. `data.status` is 'pending' until the company's signing authority approves `data.delegation_url`. `data.agent_principal` carries the principal's POST-request lifecycle state (bound system_user_id + status) so no follow-up GET is needed. `data.warnings` is ALWAYS present — empty on the clean path; non-empty when a local follow-up write degraded after the upstream broker succeeded (see the enum descriptions). A 201 with warnings still means the Altinn delegation EXISTS — reconcile, never blind-retry.

- object
  - `success` true, required
  - `data` object, required
    - `system_user_id` string, required: Altinn-issued systembruker identifier for the brokered delegation. On the clean path this same value is bound (write-once) onto the agent principal.
    - `status` 'pending' | 'active' | 'expired' | 'revoked', required: Delegation lifecycle state. New delegations always start as 'pending' until the signing authority approves.
    - `scopes` string[], required: Scopes Altinn recorded for this delegation. May be a subset of the requested list if any were rejected.
    - `valid_until` string, date-time, nullable, required: ISO 8601 UTC timestamp when the delegation expires. Null for indefinite.
    - `delegation_url` string, uri, required: Approval URL. The organisation's signing authority must visit this URL and approve the delegation before the systembruker can act.
    - `label` string, nullable, required: Echo of the consumer-supplied label (null if omitted) — symmetric contract, no follow-up GET needed.
    - `agent_principal` object, required: The agent principal's state AFTER this request. On the clean path a 'pending' principal is now 'active' with `system_user_id` bound; when `warnings` is non-empty the principal may be unchanged (activation always reflects a durable binding).
      - `id` string, uuid, required
      - `status` 'pending' | 'active' | 'suspended' | 'revoked', required: Principal lifecycle status after this request.
      - `system_user_id` string, nullable, required: The principal's write-once systembruker binding. Null when no durable binding exists after this request: the bind write degraded, or the binding was deliberately skipped because the delegation persist failed — see `warnings`.
    - `warnings` string[], required: Machine-readable degradation tokens (closed set, append-only). `delegation_persist_failed` — the local delegations row insert failed; the brokered result is returned so the approval URL is not lost, and the principal is left untouched. `principal_binding_failed` — the system_user_id bind write failed; principal status unchanged. `principal_already_bound_mismatch` — the principal already carries a DIFFERENT write-once systembruker binding; the new delegation row is persisted but the principal binding is unchanged. `principal_activation_failed` — the binding is durable but the pending→active flip failed; re-inspect the principal. `receipt_generation_failed` — the delegation is durably persisted but signing or persisting the delegation_granted receipt failed; `receipt_id` and `signed_receipt` are null (the grant itself stands — receipt failure is never fatal to the delegation). Empty array on the clean path.
    - `receipt_id` string, uuid, nullable, required: The signed delegation receipt's id (also carried INSIDE `signed_receipt` — the id is part of the signed HMAC domain). Null when the local delegation persist degraded (no durable grant to prove) or when receipt generation failed — see `warnings`.
    - `signed_receipt` SignedDelegationReceipt, required: HMAC-signed fullmakt delegation receipt — cryptographic proof that at `signed_at`, consumer `consumer_id` obtained (`delegation_granted`) or revoked (`delegation_revoked`) a scoped systembruker delegation for `org_number`. Signed through the SAME HMAC-SHA256-v1 core as the filing `SignedReceipt` (one crypto primitive, two payload domains): the signature covers EVERY field except `signature` itself — `signature_algorithm` INCLUDED, so flipping the algorithm tag without recomputing the signature fails verification (downgrade-attack defence). Verification model (be precise about the guarantee): the HMAC secret never leaves Apier, so this receipt is APIER-VERIFIABLE integrity — Apier's attestation, re-checkable by Apier at any time — NOT an independently/offline consumer-verifiable signature (that would require an asymmetric scheme; the versioned `signature_algorithm` field leaves room for such a migration). Holding the receipt still pins every signed field: any later tampering is provable the moment Apier re-verifies. The signature is safe to hold client-side (it contains no secret material). Emitted inline by POST /v1/fullmakt/request (201, event_type=delegation_granted) and POST /v1/fullmakt/revoke (200, event_type=delegation_revoked, fresh revocations only); re-fetchable via GET /v1/fullmakt/receipts, whose entries nest this exact object — the shape Apier's verifier accepts.
      - `receipt_id` string, uuid, required: Route-minted receipt identifier — INSIDE the signed HMAC domain (minted before signing, so the id itself is tamper-evident). Unique per receipt.
      - `event_type` 'delegation_granted' | 'delegation_revoked', required: Which fullmakt lifecycle event this receipt proves. `delegation_granted` — a delegation was brokered and durably recorded. `delegation_revoked` — a live delegation was durably revoked (revoke-by-insert marker). Closed set.
      - `consumer_id` string, uuid, required: The Apier consumer that held the API key which performed the event — the C in "consumer C obtained a delegation for org O".
      - `org_number` string, required: Norwegian organisation number of the CUSTOMER company the delegation applies to (Brønnøysund format, exactly 9 digits).
      - `system_user_id` string, nullable, required: The Altinn systembruker the delegation binds — the S in "under systembruker S". Nullable in the schema for forward-compatibility; both current emitters always carry the bound id.
      - `agent_principal_id` string, uuid, required: The agent principal the lifecycle event anchors to.
      - `scopes` string[], required: Scope set the delegation carried at event time — what "scoped" means in the proof. Frozen verbatim; part of the signed domain.
      - `granted_packages` string[], nullable, required: PROVISIONAL Altinn access packages the delegation row carried (intended-mapping-derived, NOT Altinn-confirmed). Null = unknown. Frozen verbatim so the receipt records what Apier believed at event time.
      - `delegation_status` 'pending' | 'active' | 'expired' | 'revoked', required: Delegation lifecycle state AT SIGNING TIME — 'pending'/'active' on a grant (pending until the signing authority approves), 'revoked' on a revocation.
      - `valid_until` string, date-time, nullable, required: Delegation expiry at event time (null = indefinite). On a delegation_revoked receipt this is the REVOCATION instant — when the authority ended. Millisecond-precision UTC (the exact string form is inside the signed domain).
      - `audit_log_id` string, uuid, required: The immutable audit_log row for the fullmakt.request / fullmakt.revoke action that produced this receipt — the receipt-to-audit forensic join.
      - `correlation_id` string, uuid, required: X-Correlation-ID of the originating request. Joins audit_log / provenance_log on the shared forensic axis.
      - `signed_at` string, date-time, required: When the receipt was signed — the T in "at time T". Millisecond-precision UTC; inside the signed domain.
      - `signature_algorithm` 'HMAC-SHA256-v1', required: Versioned algorithm tag — INSIDE the signed domain (downgrade-attack defence). A future rotation introduces a new enum value; historical receipts keep verifying under the tag they were signed with.
      - `signature` string, required: HMAC-SHA256 hex over the canonical-JSON serialisation of every other field. Verifiable ONLY by Apier (the HMAC secret never leaves the server) — treat a verified receipt as Apier's attestation, never as a third-party- or offline-consumer-verifiable signature.
  - `_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 — malformed JSON body, non-9-digit org_number, empty scopes, out-of-range validity_days, or an unknown key (the body schema is strict).
- `401`: Authentication failed — missing, malformed, invalid, or revoked API key.
- `403`: The API key lacks the fullmakt:write scope (error_code=SCOPE_INSUFFICIENT). The three fullmakt WRITE operations (create principal, request, revoke) take the assignable `fullmakt:write` scope, chosen at key creation — the default `read:*` wildcard does NOT grant it, so a read key is refused here. Unlike the legacy /v1/auth/system-user/delegate surface (which is reserved-scope-gated and 403s unconditionally), this is an ordinary insufficient-scope refusal: mint a key with `fullmakt:write` via POST /api/v1/account/keys.
- `404`: No agent principal with this id exists for the calling consumer (error_code=FULLMAKT_PRINCIPAL_NOT_FOUND). An unknown id and another consumer's id return this IDENTICAL response — existence is not disclosed across consumers.
- `409`: Two discriminated scenarios — `error_code` is the discriminator. `FULLMAKT_PRINCIPAL_NOT_ELIGIBLE`: the principal is 'revoked' (terminal — create a new principal) or 'suspended' (reactivate first); only 'pending' and 'active' principals may request a delegation. `IDEMPOTENCY_IN_PROGRESS`: another request with the same Idempotency-Key is still mid-flight — 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`: Rate limit reached on at least one of TWO independent dimensions running in parallel: per-(api_key, endpoint) per-minute (the established per-tier scaling — Free / Starter / Professional / Enterprise unlimited) AND per-(api_key, org_number) per-day (bounds runaway-agent failure modes per client org so one org's traffic cannot starve siblings sharing the same key; daily bucket window is one calendar day in Europe/Oslo, DST-aware). The `X-RateLimit-Dimension` header identifies which bucket tripped: `per_key`, `per_org`, or `both` (when the same request exceeds both — the binding Retry-After is the per-org value because the per-org window is hours/days vs the per-minute window). The `Retry-After` header indicates how many seconds to wait before the binding window resets; `X-RateLimit-Limit` / `X-RateLimit-Remaining` / `X-RateLimit-Reset` describe the per-minute bucket state. The per-day per-org limits per tier (sized at ~100× typical daily volume — the dimension exists to bound failure modes, not to monetize) are: Free 1000/org/day, Starter 5000, Professional 25000, Enterprise unlimited. On rare infrastructure-side failures the gateway fails OPEN and passes the request through with `X-RateLimit-Check: skipped` on the response — clients can treat that as a transient signal that quota enforcement was briefly degraded.
- `500`: Unexpected server failure (principal lookup infrastructure error, or a non-structured adapter failure). Internal details are never leaked in the response body. Nothing was persisted and the principal is unchanged; clients may retry.
- `502`: Altinn upstream failure while brokering the delegation (error_code=FULLMAKT_UPSTREAM_FAILED). No delegation row was written and the agent principal is unchanged. When Altinn ACKNOWLEDGED the request at HTTP 2xx but the response body was unreadable, the same code is returned with an explanation instructing manual reconciliation — the delegation MAY already exist upstream, so do NOT blind-retry that variant.
- `503`: The idempotency reservation service is temporarily unavailable (error_code=UPSTREAM_UNAVAILABLE). Clients using Idempotency-Key should retry with the same key.

---

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