---
title: "Public sandbox execute (zero-auth, IP rate-limited, dry-run + live mock)"
method: POST
path: "/api/v1/sandbox/public/actions/execute"
tags: ["Sandbox"]
---

# Public sandbox execute (zero-auth, IP rate-limited, dry-run + live mock)

`POST /api/v1/sandbox/public/actions/execute`

## Request

- Base URL: `https://www.apier.no`
- URL: `https://www.apier.no/api/v1/sandbox/public/actions/execute`
- Auth: none required

Unauthenticated. Per-IP rate limit 100 requests / hour. POST body capped at 32 KiB. Org 999999999 only. Two paths discriminated by `?dry_run=`: dry-run returns a deterministic validation block that validates `payload` against the per-action schema and the sandbox magic values (an invalid payload answers 200 with `validation.all_passed: false` and `data_format_valid` failed); live runs the same approval-token gauntlet as the internal sandbox execute (presence → prefix → length → hex-only → re-derive → constant-time compare) and returns a deterministic receipt envelope. `Idempotency-Key` header is REQUIRED on every call (matches the internal /v1/sandbox/actions/execute + production write-side contract); a missing header returns 400 VALIDATION_FAILED with `details.field='Idempotency-Key'`. Sandbox is deterministic-by-construction so the key is validated for charset / length but NOT bound to a single-use cache. PROVENANCE EXEMPT. Unlike the internal sandbox execute, this surface does NOT emit the `execution_guarantee` + `outcome` reliability envelope at the top level — public sandbox is shape-discovery, not reliability drill. Unknown query parameters return 400 VALIDATION_FAILED (the only accepted keys are `simulate_error` and the parameters documented on this operation). A 200 carries X-RateLimit-Limit / -Remaining / -Reset for the per-IP clock-hour bucket; a 429 states the same 100-per-clock-hour limit, resetting at the top of the hour.

## Query parameters

- `simulate_error` 'missing_delegation' | 'invalid_token' | 'validation_error' | 'scope_missing' | 'altinn_timeout' | 'invalid_certificate' | 'malformed_payload' | 'upstream_5xx' | 'AUTH_MISSING_DELEGATION' | 'AUTH_EXPIRED_TOKEN' | 'VALIDATION_FAILED' | 'SCOPE_MISSING'
- `dry_run` 'true' | 'false'

## Headers

- `Idempotency-Key` string, required
- `X-Correlation-ID` string, uuid

## Request body

- object
  - `org_number` string, required
  - `action` 'mva_melding' | 'a_melding', required
  - `payload` object, required
  - `approval_token` string: Boundary regex is permissive (1-255 printable ASCII); the live-mode gauntlet enforces the canonical sandbox-prefixed shape after.

## Response `200`

Success. Discriminated by `data.dry_run` boolean. Dry-run: `{ success: true, data: { dry_run: true, org_number, action, validation }, _meta }`. Live: `{ success: true, data: { dry_run: false, receipt, _sandbox_marker }, _meta }` — `_sandbox_marker` lives inside `data` (paired with the nested `data.receipt.government_response_raw._sandbox_marker`) so an agent serialising any subset of `data` still sees the marker. The internal /v1/sandbox/actions/execute route additionally exposes a top-level `_sandbox_marker` sibling; this public-sandbox variant intentionally narrows to the `data`-scoped marker since the response envelope is wrapper-stable across both branches.

- union: Discriminated by `data.dry_run`. Split into a proper `oneOf` so the dry-run vs live discriminator is preserved for generated clients (the prior `data: { additionalProperties: true }` erased it), and `_sandbox_marker` sits at the response envelope level on the live branch matching the runtime emission.
  - object
    - `success` unknown, required
    - `data` object, required
      - `dry_run` unknown, required
      - `org_number` string, required
      - `action` string, required
      - `validation` object, required: Deterministic 5-step validation block — `all_passed`, `checks[]`, `disclaimer`.
    - `_meta` SandboxMeta, required: Sandbox `_meta` block. NOT a drop-in for production's `TrustMetadata`; clients must branch on `is_sandbox` before assuming any sandbox/production field equivalence. Differences from production: 1. `is_sandbox: true` — load-bearing detection signal; `body._meta.is_sandbox === true` is a single property assertion agents use to verify they're talking to the sandbox. Production omits this key entirely. 2. `rulebook_version` is the literal string `"sandbox"` — production carries the actual Rulebook version (e.g. `"v1.0.0"`). Sandbox responses are not Rulebook-evaluated. 3. `data_freshness` and `last_verified` are pinned constants (2026-01-01T00:00:00+01:00) — production reflects actual cache / fetch state. 4. `response_timestamp` is RFC 3339 with explicit Europe/Oslo offset (`+01:00` CET / `+02:00` CEST, DST-aware) — production's `response_timestamp` carries a UTC `Z` suffix. 5. `response_hash` and `source_snapshot_id` are OMITTED — sandbox writes ZERO provenance_log rows, so a hash without a matching forensic row would be misleading. Only `source` (`"apier.no"`) and `schema_version` semantics are shared with production.
      - `rulebook_version` 'sandbox', required: Always literal `sandbox` — sandbox responses are not Rulebook-evaluated.
      - `data_freshness` '2026-01-01T00:00:00+01:00', date-time, required: Pinned constant (2026-01-01T00:00:00+01:00) for determinism — same value on every call. Schema-locked via `enum` so spec-based drift checks catch any deviation.
      - `last_verified` '2026-01-01T00:00:00+01:00', date-time, required: Pinned constant — equal to data_freshness. Schema-locked via `enum` for the same drift-detection reason.
      - `source` 'apier.no', required: Response origin. Always `apier.no`. Preserves `_meta.source` parity with production `TrustMetadata.source` so clients can reuse `_meta` parsing across both surfaces. Provenance-specific fields (`response_hash`, `source_snapshot_id`) are omitted by design.
      - `schema_version` string, required: OpenAPI document version — mirrors the top-level `info.version` field. Distinct from `/api/v1/capabilities._meta.schema_version` and `/workflows.json.schema_version`, which version the capabilities-discovery and workflows-discovery surfaces respectively. Equality with `info.version` is enforced at CI time.
      - `is_sandbox` true, required: Load-bearing sandbox-detection signal. Always `true`.
      - `response_timestamp` string, date-time, required: RFC 3339 timestamp with explicit Europe/Oslo offset — runtime emits via `formatInTimeZone(now, 'Europe/Oslo', "yyyy-MM-dd'T'HH:mm:ssXXX")` so the offset is DST-aware (`+01:00` in CET / `+02:00` in CEST). The ONLY non-deterministic field permitted on sandbox responses (excluded from determinism comparisons).
      - `sandbox_notice` string, required: Human-readable mock-vs-government honesty signal (item 5). Constant prose: a passing sandbox run is an Apier internal mock (not Altinn TT02 / Skatteetaten test) and does not guarantee real government acceptance. `is_sandbox` is the machine-readable twin; this is the prose an agent or a human reading a logged response sees. Plain ASCII so it never trips the docs bokmål canary.
      - `cost_nok` 0: Always `0`. A sandbox call never debits anything — there is no ledger write and no government upstream. Same field name and rendering as production's `TrustMetadata.cost_nok`, so a client that reads the cost off `_meta` works unchanged against both surfaces. Emitted on every sandbox response. Declared optional in this schema only so the many older sandbox examples in this document stay valid; the runtime builder emits it on every response.
      - `wallet_remaining_nok` unknown
      - `as_of` string: Present ONLY when `?as_of=` Rulebook time travel is active on /deadlines or /obligations. Echoes the agent's exact YYYY-MM-DD input so a transcript is self-describing about which reference date the deadline / obligation statuses were derived against. Absent on every other sandbox response.
      - `simulated` true: Present + `true` ONLY on an `X-Apier-Simulate` injected failure response. The load-bearing signal that a 500 / 504 / 429 is a deliberately-simulated infrastructure failure, not a real one. Absent on every real sandbox response.
      - `session_merged` true: Present + `true` ONLY when this response merged per-suffix session state (mock filings recorded by a prior POST /api/v1/sandbox/actions/execute under the SAME sandbox-bearer suffix) over the static fixture, on the three opted-in reads (/audit, /obligations, /summary). The honesty marker distinguishing fixture state from session state. Absent on every fresh-suffix response, which stays byte-deterministic.
      - `session_merged_rows` integer: Count of response items the merged session state changed (obligation/deadline statuses overridden plus audit rows prepended). Present ONLY alongside `session_merged: true`, always >= 1.
  - object
    - `success` unknown, required
    - `data` object, required
      - `dry_run` unknown, required
      - `receipt` object, required: Receipt envelope: `altinn_receipt_id` (HMAC-derived 32-char hex), `audit_log_id` (UUID v5 from sandbox namespace), `timestamp` (Europe/Oslo offset), `org_number`, `filing_type`, `rule_version: 'sandbox'`, `government_response_raw` (carries the nested `_sandbox_marker`), `signature` (HMAC over canonical receipt message), `signature_algorithm: 'HMAC-SHA256-v1'`.
      - `_sandbox_marker` 'SANDBOX_NOT_REAL_GOV_RESPONSE', required: Nested marker inside `data` — paired with the envelope-level marker for defense in depth (partial serialisations always surface at least one).
    - `_meta` SandboxMeta, required: Sandbox `_meta` block. NOT a drop-in for production's `TrustMetadata`; clients must branch on `is_sandbox` before assuming any sandbox/production field equivalence. Differences from production: 1. `is_sandbox: true` — load-bearing detection signal; `body._meta.is_sandbox === true` is a single property assertion agents use to verify they're talking to the sandbox. Production omits this key entirely. 2. `rulebook_version` is the literal string `"sandbox"` — production carries the actual Rulebook version (e.g. `"v1.0.0"`). Sandbox responses are not Rulebook-evaluated. 3. `data_freshness` and `last_verified` are pinned constants (2026-01-01T00:00:00+01:00) — production reflects actual cache / fetch state. 4. `response_timestamp` is RFC 3339 with explicit Europe/Oslo offset (`+01:00` CET / `+02:00` CEST, DST-aware) — production's `response_timestamp` carries a UTC `Z` suffix. 5. `response_hash` and `source_snapshot_id` are OMITTED — sandbox writes ZERO provenance_log rows, so a hash without a matching forensic row would be misleading. Only `source` (`"apier.no"`) and `schema_version` semantics are shared with production.
      - `rulebook_version` 'sandbox', required: Always literal `sandbox` — sandbox responses are not Rulebook-evaluated.
      - `data_freshness` '2026-01-01T00:00:00+01:00', date-time, required: Pinned constant (2026-01-01T00:00:00+01:00) for determinism — same value on every call. Schema-locked via `enum` so spec-based drift checks catch any deviation.
      - `last_verified` '2026-01-01T00:00:00+01:00', date-time, required: Pinned constant — equal to data_freshness. Schema-locked via `enum` for the same drift-detection reason.
      - `source` 'apier.no', required: Response origin. Always `apier.no`. Preserves `_meta.source` parity with production `TrustMetadata.source` so clients can reuse `_meta` parsing across both surfaces. Provenance-specific fields (`response_hash`, `source_snapshot_id`) are omitted by design.
      - `schema_version` string, required: OpenAPI document version — mirrors the top-level `info.version` field. Distinct from `/api/v1/capabilities._meta.schema_version` and `/workflows.json.schema_version`, which version the capabilities-discovery and workflows-discovery surfaces respectively. Equality with `info.version` is enforced at CI time.
      - `is_sandbox` true, required: Load-bearing sandbox-detection signal. Always `true`.
      - `response_timestamp` string, date-time, required: RFC 3339 timestamp with explicit Europe/Oslo offset — runtime emits via `formatInTimeZone(now, 'Europe/Oslo', "yyyy-MM-dd'T'HH:mm:ssXXX")` so the offset is DST-aware (`+01:00` in CET / `+02:00` in CEST). The ONLY non-deterministic field permitted on sandbox responses (excluded from determinism comparisons).
      - `sandbox_notice` string, required: Human-readable mock-vs-government honesty signal (item 5). Constant prose: a passing sandbox run is an Apier internal mock (not Altinn TT02 / Skatteetaten test) and does not guarantee real government acceptance. `is_sandbox` is the machine-readable twin; this is the prose an agent or a human reading a logged response sees. Plain ASCII so it never trips the docs bokmål canary.
      - `cost_nok` 0: Always `0`. A sandbox call never debits anything — there is no ledger write and no government upstream. Same field name and rendering as production's `TrustMetadata.cost_nok`, so a client that reads the cost off `_meta` works unchanged against both surfaces. Emitted on every sandbox response. Declared optional in this schema only so the many older sandbox examples in this document stay valid; the runtime builder emits it on every response.
      - `wallet_remaining_nok` unknown
      - `as_of` string: Present ONLY when `?as_of=` Rulebook time travel is active on /deadlines or /obligations. Echoes the agent's exact YYYY-MM-DD input so a transcript is self-describing about which reference date the deadline / obligation statuses were derived against. Absent on every other sandbox response.
      - `simulated` true: Present + `true` ONLY on an `X-Apier-Simulate` injected failure response. The load-bearing signal that a 500 / 504 / 429 is a deliberately-simulated infrastructure failure, not a real one. Absent on every real sandbox response.
      - `session_merged` true: Present + `true` ONLY when this response merged per-suffix session state (mock filings recorded by a prior POST /api/v1/sandbox/actions/execute under the SAME sandbox-bearer suffix) over the static fixture, on the three opted-in reads (/audit, /obligations, /summary). The honesty marker distinguishing fixture state from session state. Absent on every fresh-suffix response, which stays byte-deterministic.
      - `session_merged_rows` integer: Count of response items the merged session state changed (obligation/deadline statuses overridden plus audit rows prepended). Present ONLY alongside `session_merged: true`, always >= 1.

## Other responses

- `400`: 4xx / 5xx error from a public-sandbox route (`/api/v1/sandbox/public/*`). Body shape uses the public-surface error_code vocabulary defined in `PublicSandboxEnvelopeError` (adds `PUBLIC_SANDBOX_ORG_NOT_PERMITTED`, `PUBLIC_SANDBOX_BODY_TOO_LARGE`, `RATE_LIMIT_EXCEEDED` on top of the internal sandbox set). Response carries the public-sandbox-specific header set: `Access-Control-Allow-Methods: GET, POST, OPTIONS`, `X-Robots-Tag: noindex`, `X-Apier-Sandbox: public`. `Access-Control-Allow-Credentials` is INTENTIONALLY absent. On 429 responses, `Retry-After` carries the seconds-until-bucket-reset for the per-IP rate-limit window.
- `401`: 4xx / 5xx error from a public-sandbox route (`/api/v1/sandbox/public/*`). Body shape uses the public-surface error_code vocabulary defined in `PublicSandboxEnvelopeError` (adds `PUBLIC_SANDBOX_ORG_NOT_PERMITTED`, `PUBLIC_SANDBOX_BODY_TOO_LARGE`, `RATE_LIMIT_EXCEEDED` on top of the internal sandbox set). Response carries the public-sandbox-specific header set: `Access-Control-Allow-Methods: GET, POST, OPTIONS`, `X-Robots-Tag: noindex`, `X-Apier-Sandbox: public`. `Access-Control-Allow-Credentials` is INTENTIONALLY absent. On 429 responses, `Retry-After` carries the seconds-until-bucket-reset for the per-IP rate-limit window.
- `404`: 4xx / 5xx error from a public-sandbox route (`/api/v1/sandbox/public/*`). Body shape uses the public-surface error_code vocabulary defined in `PublicSandboxEnvelopeError` (adds `PUBLIC_SANDBOX_ORG_NOT_PERMITTED`, `PUBLIC_SANDBOX_BODY_TOO_LARGE`, `RATE_LIMIT_EXCEEDED` on top of the internal sandbox set). Response carries the public-sandbox-specific header set: `Access-Control-Allow-Methods: GET, POST, OPTIONS`, `X-Robots-Tag: noindex`, `X-Apier-Sandbox: public`. `Access-Control-Allow-Credentials` is INTENTIONALLY absent. On 429 responses, `Retry-After` carries the seconds-until-bucket-reset for the per-IP rate-limit window.
- `413`: 4xx / 5xx error from a public-sandbox route (`/api/v1/sandbox/public/*`). Body shape uses the public-surface error_code vocabulary defined in `PublicSandboxEnvelopeError` (adds `PUBLIC_SANDBOX_ORG_NOT_PERMITTED`, `PUBLIC_SANDBOX_BODY_TOO_LARGE`, `RATE_LIMIT_EXCEEDED` on top of the internal sandbox set). Response carries the public-sandbox-specific header set: `Access-Control-Allow-Methods: GET, POST, OPTIONS`, `X-Robots-Tag: noindex`, `X-Apier-Sandbox: public`. `Access-Control-Allow-Credentials` is INTENTIONALLY absent. On 429 responses, `Retry-After` carries the seconds-until-bucket-reset for the per-IP rate-limit window.
- `429`: 4xx / 5xx error from a public-sandbox route (`/api/v1/sandbox/public/*`). Body shape uses the public-surface error_code vocabulary defined in `PublicSandboxEnvelopeError` (adds `PUBLIC_SANDBOX_ORG_NOT_PERMITTED`, `PUBLIC_SANDBOX_BODY_TOO_LARGE`, `RATE_LIMIT_EXCEEDED` on top of the internal sandbox set). Response carries the public-sandbox-specific header set: `Access-Control-Allow-Methods: GET, POST, OPTIONS`, `X-Robots-Tag: noindex`, `X-Apier-Sandbox: public`. `Access-Control-Allow-Credentials` is INTENTIONALLY absent. On 429 responses, `Retry-After` carries the seconds-until-bucket-reset for the per-IP rate-limit window.
- `500`: 4xx / 5xx error from a public-sandbox route (`/api/v1/sandbox/public/*`). Body shape uses the public-surface error_code vocabulary defined in `PublicSandboxEnvelopeError` (adds `PUBLIC_SANDBOX_ORG_NOT_PERMITTED`, `PUBLIC_SANDBOX_BODY_TOO_LARGE`, `RATE_LIMIT_EXCEEDED` on top of the internal sandbox set). Response carries the public-sandbox-specific header set: `Access-Control-Allow-Methods: GET, POST, OPTIONS`, `X-Robots-Tag: noindex`, `X-Apier-Sandbox: public`. `Access-Control-Allow-Credentials` is INTENTIONALLY absent. On 429 responses, `Retry-After` carries the seconds-until-bucket-reset for the per-IP rate-limit window.

---

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