> ## Documentation Index
> Fetch the complete documentation index at: https://docs.scan.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Event catalog, signatures, retries, and ordering

Scan POSTs JSON to the `https://` `webhook_url` on your `api_credentials`.

| Event               | Header `X-Scan-Event`          | Payload                           | PHI |
| ------------------- | ------------------------------ | --------------------------------- | --- |
| Visit status        | `visit.status_changed`         | Referral snapshot + status change | Yes |
| Notification status | `notification.status_changed`  | Metadata only                     | No  |
| Auto-book completed | `referral.auto_book_completed` | Booking outcome                   | Yes |

Visit- and notification-status events include an `event_id` that is stable
across retries; use it to deduplicate. Auto-book completion does not currently
include `event_id`, so make those handlers idempotent on
`auto_book_request.id` and its terminal `status`. `occurred_at` is reused on
retry. Delivery is **at least once**. **Ordering is not guaranteed** — a
`booked` event can arrive after `canceled` if the first delivery was delayed.
`visits[].status` on a subsequent GET is the source of truth.

Scan retries each subscriber up to **five** times with exponential backoff, 5s
connect / 15s read timeout. After five failures the delivery is marked `failed`. There
is no automatic replay after an extended outage; use
[test events](/guides/testing-and-sandbox) or GET the referral.

## Verify the signature

`X-Scan-Signature` is `v1=<hex>` or `v1=<hex>,v1=<hex>` during secret rotation.
Compute HMAC-SHA256 of `#{timestamp}.#{raw_body}` with your signing secret.

<CodeGroup>
  ```ruby Ruby theme={"dark"}
  OpenSSL::HMAC.hexdigest("SHA256", secret, "#{timestamp}.#{body}")
  ```

  ```python Python theme={"dark"}
  import hmac, hashlib
  hmac.new(secret.encode(), f"{timestamp}.{body}".encode(), hashlib.sha256).hexdigest()
  ```

  ```javascript JavaScript theme={"dark"}
  crypto.createHmac("sha256", secret).update(`${timestamp}.${body}`).digest("hex");
  ```

  ```php PHP theme={"dark"}
  hash_hmac('sha256', $timestamp . '.' . $body, $secret);
  ```

  ```go Go theme={"dark"}
  mac := hmac.New(sha256.New, []byte(secret))
  mac.Write([]byte(timestamp + "." + body))
  hex.EncodeToString(mac.Sum(nil))
  ```

  ```bash cURL theme={"dark"}
  # Compare the hex HMAC of "$timestamp.$body" to each v1= value
  ```
</CodeGroup>

Reject if the timestamp is older than five minutes. Rotate the secret on the
**API access** tab; Scan keeps the previous secret valid for 24 hours.

Configure visit vs notification delivery independently. Notification events also
need the `referrals_notifications_webhook` capability.
