# Change Webhooks

Verzi POSTs the archive change feed to your HTTPS endpoint. Every event
carries its receipt: the document id, the version, and the sha256 of the
archived bytes. You can audit any event against the public archive.

## Register

Webhook subscriptions require an API key. One request registers the URL
and returns the signing secret. The secret is shown once and never again.

```bash
curl -X POST https://api.verzi.health/v1/subscriptions \
  -H "X-API-Key: $VERZI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "webhook",
    "webhook_url": "https://example.com/hooks/verzi",
    "codes": ["K0606", "A4239"]
  }'
```

Scope options:
- `codes`: HCPCS codes. You get changes to documents that govern these codes.
- `source_ids`: archive sources (see `/v1/archive/sources`).
- Neither: every public source.

URL rules: `https` only, no credentials in the URL, no private address.
The resolved address is checked again at delivery time.

## Delivery

One POST per run (daily, 14:00 UTC) when at least one change matches.
No matching changes, no request.

Headers:

| Header | Value |
|---|---|
| `Content-Type` | `application/json` |
| `X-Verzi-Event` | `changes.observed` |
| `X-Verzi-Subscription` | your subscription id |
| `X-Verzi-Signature` | `hex(hmac_sha256(secret, raw_body))` |

Body:

```json
{
  "event": "changes.observed",
  "sent_at": "2026-10-06T14:00:12+00:00",
  "subscription_id": 42,
  "count": 2,
  "changes": [
    {
      "doc_id": "lcd:33718",
      "source_id": "mcd_archive",
      "title": "Oxygen and Oxygen Equipment",
      "publisher_version": "R15",
      "version_id": 8123,
      "publisher_effective_from": "2026-11-01",
      "publisher_effective_to": null,
      "observed_from": "2026-10-06T08:14:02+00:00",
      "origin": "live",
      "sha256": "9f2c...64 hex...",
      "affected_codes": ["E0601", "K0606"],
      "versions_url": "https://api.verzi.health/v1/archive/docs/lcd:33718/versions"
    }
  ]
}
```

Field notes:
- `sha256` is the hash of the archived original bytes. Verify it against
  `GET /v1/archive/files/{sha256}`.
- `publisher_effective_from` in the future means the version takes effect
  on that date. You hear about it when the archive observes it, not on
  the effective day.
- `origin` other than `live` (`backfill`, `wayback`) means the version is
  new to the archive, not newly published.
- `affected_codes` comes from Verzi's coverage mapping. It is empty when
  no mapping exists.

## Verify the signature

Compute the HMAC over the raw request body and compare in constant time.

```python
import hashlib, hmac

def verify(secret: str, raw_body: bytes, signature_header: str) -> bool:
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature_header)
```

Reject the request when the signature does not match. The body is signed
exactly as sent; parse it only after the check passes.

## Retries and ordering

- A delivery succeeds on any 2xx within 10 seconds. Redirects are not
  followed and count as failure.
- On failure, the cursor does not advance. The next daily run redelivers
  the same changes plus any new ones. Delivery is at-least-once:
  **dedupe on `version_id`**.
- The redelivery window is capped at 14 days. After that, read the feed:
  `GET /v1/archive/changes`.

## Unregister

`POST /v1/subscriptions/unsubscribe?token=<unsubscribe_token>` with the
token from the registration response.
