ONVY webhooks let your integration react to changes without polling.
Configuration shape
Webhook delivery is configured per project:
{
"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:
{
"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.
X-Webhook-Signature
X-Webhook-Timestamp
X-Webhook-ID
The signature uses HMAC-SHA256 and the format sha256=<hex_digest>.
Verify signatures
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.
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.