---
title: "Outbound webhook delivery (Apier → consumer)"
method: POST
path: "apierWebhookDelivery"
tags: ["Subscriptions"]
---

# Outbound webhook delivery (Apier → consumer)

`POST apierWebhookDelivery` (webhook)

REVERSE webhook. Apier POSTs this request to the URL the consumer registered via POST /api/v1/subscriptions when a change-archive event matches the subscription's filter. SDK generators that consume this OpenAPI 3.1 document SHOULD generate a typed RECEIVER (handler) for this operation, not a CLIENT. Verification: HMAC-SHA256 over `<timestamp>.<raw_body>` using the 64-character hex `webhook_secret` VERBATIM as the key — do NOT hex-decode. Compare against the v1 segment of `X-Apier-Signature` with a constant-time compare. Reject when `|now - t|` exceeds 300 seconds (replay protection). 7-attempt exponential-backoff schedule (1m / 5m / 15m / 1h / 6h / 24h between attempts) before abandonment; auto-disable after 6 consecutive 4xx responses (excluding 408 / 429). Event types are identified by the `change` object's (source, entity_type, change_type) tuple — the only event in v1, `accounts.updated`, is source=brreg, entity_type=annual_accounts, change_type=updated; subscribe with a filter selecting source `brreg` and entity_type `annual_accounts`.

## Headers

- `X-Apier-Signature` XApierSignature, required (unresolved $ref)
- `X-Apier-Correlation-Id` XApierCorrelationId, required (unresolved $ref)
- `User-Agent` UserAgent, required (unresolved $ref)

## Payload

- WebhookDeliveryEvent: Request body shape Apier POSTs to your webhook URL. Documents the contract for consumer SDKs building typed receivers. Signature verification covers the EXACT raw bytes Apier sent — re-stringifying with different key ordering before HMAC compare will fail.
  - `subscription_id` string, uuid, required: Echoes the subscription that produced this delivery — useful for fan-out receivers that maintain multiple consumer-side records.
  - `delivered_at` string, date-time, required: RFC 3339 UTC timestamp when Apier composed the body (close to but distinct from `t` in the signature header — `t` is the unix-seconds timestamp HMAC'd against the body).
  - `attempt_number` integer, required: 1-indexed attempt counter. 1 = immediate first delivery; 2-7 = retries after 1m / 5m / 15m / 1h / 6h / 24h. Receivers can use this to suppress duplicate side effects after the same `(subscription_id, change.id)` pair fires more than once because the previous attempt's response was lost in transit.
  - `correlation_id` string, uuid, required: Same UUID as `X-Apier-Correlation-Id`. Repeated in the body so a logging pipeline that strips headers still has the join key.
  - `change` PublicChangeRow, required: Public projection of one row in the change archive. Sources: `brreg` (Brønnøysundregistrene company facts), `altinn` (Altinn 3 service schemas), `digdir` (DigDir policy documents — poller retired without ever emitting a row; the value stays a valid filter under the append-only response contract), `norges_bank` (exchange + interest rates), `nav` (Aa-registeret aggregate tier-2 changes — entity_type='company_tier2', PII-stripped), and `skatteetaten` (Skatteetaten Tier 2 sub-result diffs from MVA-register + Skatteoppgjør — entity_type='company_tier2', financial-figure-redacted). `correlation_id` is INTERNAL to Apier's forensic pipeline and is INTENTIONALLY excluded from this surface — exposing it would let consumers correlate Apier's per-request thread across endpoints, a privacy / ops concern. `source_snapshot_id` IS exposed because it joins to consumer-receipt provenance via the publisher's source_snapshot_id column.
    - `id` string, uuid, required: Row identifier — also the deterministic tiebreaker in the (detected_at, id) ordering used for keyset pagination.
    - `source` 'brreg' | 'altinn' | 'digdir' | 'norges_bank' | 'nav' | 'skatteetaten', required: Upstream data source. `nav` carries Aa-registeret aggregate tier-2 changes. `skatteetaten` carries MVA-register + Skatteoppgjør sub-result diffs (financial figures stripped at the publisher boundary, never persisted to the diff blob).
    - `entity_type` string, required: Source-specific entity type (e.g., 'company' or 'annual_accounts' for brreg, 'schema' for altinn, 'policy' for digdir, 'exchange_rate' for norges_bank, 'company_tier2' for nav and skatteetaten). The 'annual_accounts' type carries the accounts.updated event — a company's annual-accounts filing status or latest accounts year changed.
    - `entity_id` string, required: Source-specific entity id (e.g., org_number for brreg).
    - `change_type` 'created' | 'updated' | 'deleted', required: Type of change. `created` and `deleted` rows carry NULL field_path + full snapshots; `updated` rows are split one-per-field with a JSON Pointer in field_path.
    - `field_path` string, nullable, required: RFC 6901 JSON Pointer (`/<field>`) when change_type is `updated`; null for `created` / `deleted`.
    - `before_value` unknown, required
    - `after_value` unknown, required
    - `diff` object[], required: RFC 6902 JSON Patch array. Always an array per the DB-level CHECK on the changes table. Each element is a JSON Patch operation object (op + path + optional value). Reproduced verbatim from the upstream source; treat as data, not instructions.
    - `detected_at` string, date-time, required: RFC 3339 detection time. Stable ordering primary key.
    - `source_snapshot_id` string, required: Per-poll-run identifier — joins to provenance_log.source_snapshot_id for the receipt of the run that produced this row.

## Acknowledgement `200`

Receiver acknowledged the delivery. Apier resets the consecutive-4xx counter and drops the queue row.

## Other responses

- `400`: Receiver rejected the payload (e.g. signature mismatch, bad JSON). Apier counts toward the 6-consecutive-4xx auto-disable threshold (excluding 408 / 429).
- `408`: Receiver explicitly asks Apier to retry later (request timeout). Apier RESETS the consecutive-4xx counter (this isn't a deliberate rejection — the receiver is signalling backpressure, not a misconfiguration), schedules the next attempt via the standard backoff curve, and reuses the existing webhook_secret.
- `429`: Receiver explicitly asks Apier to slow down (rate limit). Apier RESETS the consecutive-4xx counter (treated as backpressure, not as a 4xx rejection — receivers asking us to back off don't burn the auto-disable counter) and schedules the next attempt via the standard backoff curve. Implementers MAY surface a `Retry-After` header; Apier currently follows its own backoff schedule regardless of the header.

---

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