Sandbox

Sandbox mirror of /api/v1/actions/execute (API key or sandbox bearer, dry-run + live mock)

The sandbox write-side execute endpoint. Two paths discriminated by ?dry_run=:

Dry-run (?dry_run=true): returns a deterministic validation block describing the five checks, with data.dry_run: true and NO data.altinn_receipt_id. reliability envelope (execution_guarantee + outcome at top level) emitted on every response (universal-emission contract). No approval token required.

Live mock (no dry_run, or dry_run=false): requires approval_token in the body. A successful live mock filing under a sandbox bearer is ALSO recorded in the caller's ephemeral 15-minute session state, so the next same-suffix read of the sandbox /audit, /obligations or /summary merges the Pending→Filed transition over the static fixture (_meta.session_merged). Real API keys and dry-run record nothing; no government upstream is ever called. Runs the full validation gauntlet — presence → prefix → length → hex-only → re-derive → crypto.timingSafeEqual. Returns a deterministic receipt envelope with altinn_receipt_id (HMAC-derived 32-char hex), audit_log_id (UUID v5 from sandbox namespace), signature (HMAC over canonical receipt message), and signature_algorithm: 'HMAC-SHA256-v1'. Top-level + nested _sandbox_marker field flags every sandbox receipt as synthetic.

Sandbox does NOT enforce expires_at on the approval token — sandbox is deterministic-by-construction; SANDBOX_NOW is fixed (2026-05-04T12:00:00+02:00). No DB writes to audit_log / provenance_log / receipts / approval_tokens.

?simulate_error= precedes everything except validation; reserved error orgs (999000901-904) fire BEFORE token validation so an agent can probe the explainer envelope without a valid token. On the dry-run path (?dry_run=true) a reserved error org instead answers 200 with validation.all_passed: false and the check it simulates marked failed, as production's dry-run reports a failing check; and every dry-run validates payload against the per-action schema plus the sandbox magic values, so an invalid payload answers 200 with data_format_valid failed.

Idempotency-Key is required. Mirrors the production write surface so SDK retry code paths work against either endpoint without branching. A missing header returns 400 VALIDATION_FAILED with details.field: 'Idempotency-Key'. Sandbox is deterministic-by-construction (same body produces the same receipt on every call) so replays return verbatim — but unlike production, sandbox does NOT write a was_idempotent_replay=true audit_log row on replay (sandbox writes ZERO production DB rows — the only sandbox write is the ephemeral sandbox_sessions state described above, 15-minute TTL, which a live-mock replay under a sandbox bearer records again; the idempotency-replay audit row is a production-only forensic artifact).

post/api/v1/sandbox/actions/execute

Request

  • Base URL: https://www.apier.no
  • URL: https://www.apier.no/api/v1/sandbox/actions/execute
  • Auth: one of:
    • HTTP bearer
    • HTTP bearer

Query parameters

dry_run'true' | 'false'

Strict z.enum — '1' / 'yes' / '' / garbage all return 400 (empty value rejected by, no silent fallthrough).

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'

Force the sandbox to return a structured error response. tokens (Apier-edge failures) — missing_delegation → 403 AUTH_MISSING_DELEGATION; invalid_token → 401 AUTH_EXPIRED_TOKEN; validation_error → 400 VALIDATION_FAILED; scope_missing → 403 SCOPE_MISSING. upstream-failure tokens — altinn_timeout → 504 EXECUTION_DEADLINE_EXCEEDED (Altinn 3 instances endpoint timed out, response body carries the Altinn RFC 7807 envelope); invalid_certificate → 502 MASKINPORTEN_AUTH_FAILED (Maskinporten OAuth2 error_object — invalid_client); malformed_payload → 422 GOVERNMENT_VALIDATION_REJECTED (Skatteetaten validation errors with nested path[] arrays); upstream_5xx → 502 GOVERNMENT_UNAVAILABLE (generic government 5xx). The 500 INTERNAL_ERROR path is reserved for unexpected internal failures. Accepts BOTH lowercase public tokens (primary contract) AND uppercase internal explainer codes (back-compat). Wins precedence over reserved-org defaults.

mock_datestring

Pin the sandbox's now() to a specific Europe/Oslo wall-clock date. Format strictly YYYY-MM-DD (10 chars) — RFC 3339 with time, ISO with offset, locale forms all 400. The date interprets as midnight Europe/Oslo (CET / +01:00 winter, CEST / +02:00 summer — DST-aware via date-fns-tz fromZonedTime, never hardcoded offsets). Honored on the write path (POST /actions/execute computes filed_late against the pinned date) AND on the authenticated /v1/sandbox/company/{org}/deadlines and /obligations reads, where each item's upcoming/overdue temporal status re-derives against the pinned date — the deadline DATES themselves never move (deterministic fixtures). The zero-auth /v1/sandbox/public/* mirror does not parse this knob. When ?as_of= is also present on those reads, as_of wins. Year range 2000..2100; invalid calendar dates (Feb 30, Apr 31) rejected explicitly.

Headers

Idempotency-Keystring required

REQUIRED on /api/v1/sandbox/actions/execute. The HEADER is required and validated at the boundary (1-255 printable ASCII; missing → 400 VALIDATION_FAILED with details.field: 'Idempotency-Key'). What sandbox does NOT do — distinct from production — is enforce single-use REPLAY BINDING via the idempotency_keys table: on production the same key + same body within 24h returns the cached response and emits Idempotent-Replay: true, while sandbox computes the response fresh every call (deterministic-by-construction; same body always returns the same response, so no cache is needed). The contract MATCHES production at the header level so SDK retry code paths work against either surface. § Sandbox Middleware Exemptions still applies: sandbox writes ZERO audit_log rows on replay — the was_idempotent_replay=true forensic row is a production-only artifact.

X-Correlation-IDstring uuid

Optional request-scope UUID v4 that the client can set to thread its own trace across multiple requests. A valid UUID v4 is reused verbatim on the server; anything malformed or absent is replaced with a fresh crypto.randomUUID(). The canonical value is echoed back on the X-Correlation-ID response header AND persisted on every matching audit_log row — so a forensic query can reconstruct "every row from request X" without timestamp-based correlation. Accepted on every /api/v1/* operation via the proxy middleware; no per-operation opt-in required. The header is also emitted on every /api/v1/* response — see the X-Correlation-ID response-header component.

Request body

org_numberstring required

9-digit reserved synthetic org. Must appear in the data.sandbox.reserved_test_org_numbers array on the response body of GET /api/v1/capabilities (or 999000901-904 for reserved error orgs). Any other 9-digit value returns 400 VALIDATION_FAILED — distinct from the read-side which 404s on unknown orgs (sandbox write-side invariant).

action'mva_melding' | 'a_melding' required
payloadobject required

Free-shape upstream payload. Sandbox does NOT validate per-action shape — fixture data is the only happy-path observation. Production /api/v1/actions/execute applies per-action discriminated Zod.

approval_tokenstring

REQUIRED on live mode (no dry_run, or dry_run=false). OPTIONAL on dry_run. The schema pattern is DELIBERATELY permissive at the contract boundary (printable ASCII, 1-255 chars) and NOT the canonical sandbox-prefixed shape — the route validation gauntlet (presence then prefix then length then hex-only then re-derive then constant-time compare) is what enforces the canonical format and emits structured 401 AUTH_EXPIRED_TOKEN responses for malformed tokens. Tightening the schema to the canonical regex would shift those rejections to 400 VALIDATION_FAILED, losing the AUTH error class the integration suite relies on. The route comment (Step-4 boundary Zod) records the same rationale. Description tightened to make the boundary-vs-gauntlet split visible to SDK generators / contract readers.

Response

Success. Body shape on dry_run: { success: true, data: { dry_run: true, org_number, action, validation: {...} }, execution_guarantee, outcome, _meta }. Body shape on live: { success: true, data: { dry_run: false, receipt: {...} }, execution_guarantee, outcome, _sandbox_marker, _meta }. outcome.is_final: true, outcome.requires_followup: false, outcome.success_probability: 0.99, outcome.confidence: 'high' on the happy path. Description aligned with the schema + runtime — dry-run data carries org_number + action echo fields too (the route stamps them so SDK clients can correlate the response to the request without re-reading the body).

OR

Changes

No recorded changes to this operation across all 1 revision of this API.