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

# Webhooks

> Configure ONVY webhook delivery and verify HMAC-signed event payloads.

ONVY webhooks let your integration react to changes without polling.

## Configuration shape

Webhook delivery is configured per project:

```json theme={null}
{
  "url": "https://customer.example/webhooks/onvy",
  "events": ["users:created", "daily_records:updated", "ai_summaries:created"],
  "enabled": true,
  "hmac_secret": "replace-with-a-secret-of-at-least-16-characters"
}
```

Rules:

* `url` must be HTTPS
* `events` must contain only supported event names
* `enabled` must be `true` to send deliveries
* `hmac_secret` must be at least 16 characters

## Delivery payload

One POST can contain multiple matching events. Up to `10` events are batched into a single delivery:

```json theme={null}
{
  "id": "wh_01J...",
  "created_at": "2026-03-05T18:10:27Z",
  "project_id": "proj_123",
  "org_id": "org_123",
  "api_version": 1,
  "events": [
    {
      "name": "daily_records:updated",
      "user_id": "user_123",
      "data": {
        "id": "score_123"
      }
    }
  ]
}
```

Routing metadata in the envelope (`id`, `created_at`, `project_id`, `org_id`, `api_version`, per-event `name`, optional `user_id`) is always plain JSON, even when individual event payloads are externalized.

## Headers

* `X-Webhook-Signature`
* `X-Webhook-Timestamp`
* `X-Webhook-ID`

The signature uses HMAC-SHA256 and the format `sha256=<hex_digest>`.

## Verify signatures

```python theme={null}
import hashlib
import hmac


def verify_signature(raw_body: bytes, secret: str, header_value: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode("utf-8"),
        raw_body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, header_value)
```

## Delivery semantics

* Delivery is asynchronous and **at-least-once**: your endpoint must be idempotent.
* Failed deliveries are retried with exponential backoff. The HTTP dispatch path retries on `429`, `500`, `502`, `503`, and `504` with `total=3` and `backoff_factor=1`. Internal EventBridge dispatch into the webhook path is also retry-based and protected by a DLQ.
* Action semantics are encoded in the event `name` (for example `daily_records:created`, `daily_records:updated`, `daily_records:deleted`).

## Idempotency

Use these fields to deduplicate on your side:

* The envelope `id`, also delivered as the `X-Webhook-ID` header, is unique per delivery.
* Per-event payloads carry stable resource IDs (for example a `daily_records` event's `data.id` matches the resource ID returned by the API).

Both fields are safe to use as idempotency keys.

## Large payloads

Large event payloads can be externalized. When this happens, the event carries a `url` pointing to the full payload while routing metadata stays in the envelope. Your handler should fetch the URL when present rather than rely on inline `data` only.

## Event families

The current public webhook catalog includes:

* `users:*`
* `users.data_syncs:updated` for provider sync state changes (for example deep-history loads)
* `facts:*`
* `daily_records:*`
* `ai_summaries:*` for meal, sleep, workout, daily, weekly, nutrition, trend, and impact summaries
* `meals:*`, including `meals:updated` when async nutrition analysis completes and a `summary_id` becomes available
* `custom_records:*`
* `workouts:*`
* `lab_tests:*`
* Batch lifecycle events such as `batch.succeeded`

Use the exact event names supported by your project configuration.

<Tip>
  Many domain insights such as meal nutrition analysis or sleep summaries are delivered through `ai_summaries:*` rather than as standalone event families. See `/ai-capabilities` for the type catalog.
</Tip>
