---
title: "Webhook simulator — deliver one signed mock event to your registered webhook_url"
method: POST
path: "/api/v1/sandbox/subscriptions/test"
tags: ["Sandbox"]
---

# Webhook simulator — deliver one signed mock event to your registered webhook_url

`POST /api/v1/sandbox/subscriptions/test`

## Request

- Base URL: `https://www.apier.no`
- URL: `https://www.apier.no/api/v1/sandbox/subscriptions/test`
- Auth: one of:
  - HTTP bearer
  - HTTP bearer

Fires ONE deterministic mock change event at the calling account's most recently created ACTIVE webhook subscription and returns the delivery result inline — the full round trip (receiver status code, response time, signature header name, delivery log row id) in a single call. Built for developers evaluating the Accounts Monitor / change feed who need to test their receiver TODAY instead of waiting for a real change to ride the cron pipeline.

The delivery goes through the UNCHANGED production delivery library: the webhook URL is SSRF-re-validated immediately before the request (private / loopback / metadata targets are blocked with `error_class: "ssrf_blocked"`), DNS is pinned to the validated address, 3xx redirects are never followed, the receiver has 10 seconds to answer, and the body is signed with the SAME Stripe-style `x-apier-signature` HMAC header real deliveries carry (`t=<unix_seconds>,v1=<hex_hmac>`, HMAC input `<timestamp>.<raw_body>`, keyed with the plaintext secret returned once at subscription creation). A receiver that verifies the simulator's signature verifies production's.

Three `event_type` values, each mapping to one FIXED mock change row (deterministic — byte-identical `change` object on every call; only `delivered_at` and the echoed `correlation_id` vary): `accounts_filed` mirrors the real Accounts Monitor emitter (source `brreg`, entity_type `annual_accounts`); `capability_changed` mirrors the Altinn schema poller (source `altinn`, entity_type `schema`); `deadline_elapsed` is a synthetic rehearsal of a deadline-status flip (source `skatteetaten`, entity_type `deadline`, `pending` → `overdue`). The event subject is the reserved sandbox scenario org `999111114` — never a real company.

The attempt is recorded in the append-only `webhook_deliveries` log tagged `is_sandbox: true` with NO backing `changes` row (the real change archive is never polluted with synthetic events), and the cron delivery queue is NEVER touched — the simulator delivers inline, exactly once, `attempt_number: 1`, no retries.

AUTH — dual, like the other authenticated sandbox mirrors: a real API key OR the synthetic `apier_sandbox_test_<suffix>` bearer. Note the synthetic bearer cannot own a subscription (subscriptions require a real key), so it deterministically receives the same helpful 404 an unsubscribed real key gets. The endpoint returns 200 even when the receiver rejects the delivery — the SIMULATION completed; the delivery outcome (`delivered`, `http_status`, `error_class`) is the data you asked to see.

## Query parameters

- `simulate_error` 'missing_delegation' | 'invalid_token' | 'validation_error' | 'scope_missing'

## Headers

- `Idempotency-Key` string, required

## Request body

- object
  - `event_type` 'capability_changed' | 'deadline_elapsed' | 'accounts_filed', required: Which fixed mock change event to synthesize and deliver. `accounts_filed` mirrors the real Accounts Monitor `accounts.updated` emission (the one production event type at v1) — use it to validate an Accounts Monitor receiver end-to-end. `capability_changed` and `deadline_elapsed` rehearse the same payload envelope with other source/entity_type combinations so filter-driven receivers can exercise their routing before those emitters ship. Anything else fails 400 VALIDATION_FAILED (strict schema — unknown body keys are rejected too, never silently ignored).

## Response `200`

The simulation ran to completion — read `data.delivered` for the receiver's verdict. `delivered: true` means the receiver answered 2xx; `delivered: false` carries the failure classification from the production delivery library (`error_class`: receiver 4xx/5xx → `unknown` with the status in `http_status`; SSRF-blocked target → `ssrf_blocked` with `http_status: null`; plus `timeout`, `redirect_blocked`, `dns_failure`, `tls_failure`, `network_error`, `body_too_large`). `webhook_delivery_id` is the append-only `webhook_deliveries` row recording the attempt (sandbox-tagged); `null` only if the best-effort log write failed — the delivery itself still happened as reported. Served `Cache-Control: no-store` like the whole sandbox surface.

- object
  - `success` true, required
  - `data` object, required
    - `event_type` 'capability_changed' | 'deadline_elapsed' | 'accounts_filed', required: The mock event that was synthesized and delivered — echoes the request so the response is self-describing in logs.
    - `subscription_id` string, uuid, required: The subscription the test event was delivered against — the calling account's most recently created ACTIVE subscription. Also stamped as `subscription_id` inside the delivered payload.
    - `delivered` boolean, required: `true` iff the receiver answered 2xx (mirrors the delivery library's `ok` verdict). A `false` here with `error_class: "unknown"` and a 4xx/5xx `http_status` means YOUR receiver rejected the delivery — check its logs against the `x-apier-signature` verification recipe in the webhooks guide.
    - `http_status` integer, nullable, required: HTTP status the receiver returned. `null` when no HTTP exchange completed: SSRF-blocked target, DNS/TLS/network failure, or the 10-second timeout.
    - `error_class` 'timeout' | 'dns_failure' | 'tls_failure' | 'ssrf_blocked' | 'redirect_blocked' | 'network_error' | 'body_too_large' | 'unknown' | 'null', nullable, required: Failure classification from the unchanged production delivery library; `null` on success. The same closed vocabulary `webhook_deliveries.error_class` uses, so simulator rows and real delivery rows read identically in forensic queries.
    - `response_time_ms` integer, required: Wall-clock milliseconds the delivery attempt took (URL re-validation + HTTP round trip), measured at the route. A measurement, not fixture data — varies per call like `_meta.response_timestamp`.
    - `signature_header` 'x-apier-signature', required: Name of the HMAC signature header stamped on the delivery your receiver just saw — surfaced so you know exactly which header to verify. Format `t=<unix_seconds>,v1=<hex_hmac>`; HMAC input `<timestamp>.<raw_body>`, keyed with your subscription's plaintext secret.
    - `webhook_delivery_id` string, uuid, nullable, required: Id of the append-only `webhook_deliveries` row recording this attempt, tagged `is_sandbox: true` with no backing `changes` row. `null` only when the best-effort log write failed (surfaced to Sentry; the delivery itself still happened as reported).
  - `_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`: Sandbox 4xx / 5xx error. Body shape mirrors the production error envelope (`{ success: false, error_code, explanation, _meta }`); `explanation` is the Compliance Explainer's full Norwegian-bokmål `{ summary, why, fix_steps[], relevant_link, legal_basis, handover }`. The Cache-Control header is `no-store` so transient failures aren't pinned in the Edge cache.
- `401`: Sandbox 4xx / 5xx error. Body shape mirrors the production error envelope (`{ success: false, error_code, explanation, _meta }`); `explanation` is the Compliance Explainer's full Norwegian-bokmål `{ summary, why, fix_steps[], relevant_link, legal_basis, handover }`. The Cache-Control header is `no-store` so transient failures aren't pinned in the Edge cache.
- `404`: Sandbox 4xx / 5xx error. Body shape mirrors the production error envelope (`{ success: false, error_code, explanation, _meta }`); `explanation` is the Compliance Explainer's full Norwegian-bokmål `{ summary, why, fix_steps[], relevant_link, legal_basis, handover }`. The Cache-Control header is `no-store` so transient failures aren't pinned in the Edge cache.
- `429`: Sandbox 4xx / 5xx error. Body shape mirrors the production error envelope (`{ success: false, error_code, explanation, _meta }`); `explanation` is the Compliance Explainer's full Norwegian-bokmål `{ summary, why, fix_steps[], relevant_link, legal_basis, handover }`. The Cache-Control header is `no-store` so transient failures aren't pinned in the Edge cache.
- `500`: Sandbox 4xx / 5xx error. Body shape mirrors the production error envelope (`{ success: false, error_code, explanation, _meta }`); `explanation` is the Compliance Explainer's full Norwegian-bokmål `{ summary, why, fix_steps[], relevant_link, legal_basis, handover }`. The Cache-Control header is `no-store` so transient failures aren't pinned in the Edge cache.

---

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