Skip to main content
API reference · Webhooks

Webhooks

Push audit events to your own endpoint as they happen. Deliveries are signed with HMAC-SHA-256 (Stripe-style) and retried on failure with exponential backoff.

A webhook is a URL Velora POSTs to whenever an audit event matching your filter is written. The payload is a JSON event envelope; the request carries an X-Velora-Signature header you verify with your endpoint's secret. Deliveries retry on any non-2xx response. After ten consecutive failures we mark the endpoint inactive; you re-create it after fixing.

Register an endpoint

POST /api/v2/public/audit/webhooks
Content-Type: application/json
Authorization: Bearer vlk_live_...

{
  "url": "https://your-app.example.com/velora-webhook",
  "event_filter": ["phi.", "admin."],
  "description": "Production SIEM"
}

Required scope: webhooks:write. The response includes the endpoint's id and a one-time secret:

{
  "endpoint": {
    "id": "wh_8b2e5c11",
    "url": "https://your-app.example.com/velora-webhook",
    "event_filter": ["phi.", "admin."],
    "description": "Production SIEM",
    "active": true,
    "created_at": "2026-05-08T14:22:01+00:00"
  },
  "secret": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
}
One-shot disclosure
The secret is shown exactly once. Capture it now — we cannot recover it later. If you lose the secret, delete the endpoint and re-register.

Event filter

Each entry in event_filter is a prefix. An empty array subscribes to every event your tenant emits.

FilterMatches
[]All events.
["phi."]phi.read, phi.query, phi.export, phi.decrypt.
["phi.read"]phi.read only.
["phi.", "admin."]All PHI + admin events.

Delivery payload

Every delivery POSTs a JSON envelope with the event data plus a monotonically-versioned schema marker. Tolerate unknown fields — we may add fields without bumping schema_version. Removal or rename is a breaking change and bumps the version.

{
  "type": "phi.read",
  "id": "8b2e5c11-1234-4abc-89de-0123456789ab",
  "timestamp": "2026-05-03T14:22:01+00:00",
  "tenant_id": "acme",
  "actor": { "user_id": "u-1234", "user_role": "data_analyst" },
  "resource": { "type": "claim", "id": "C-7890" },
  "phi_involved": true,
  "success": true,
  "details": { "workflow": "renewal" },
  "schema_version": "1"
}

Verify the signature

Every delivery includes an X-Velora-Signature header. The format is:

X-Velora-Signature: t=1717423320,v1=8e4f...c1a9

To verify a delivery:

  1. Parse t (unix timestamp, seconds) and v1 (hex HMAC) from the header.
  2. Reject if |now - t| > 300 seconds — protects against replay attacks.
  3. Compute expected = HMAC_SHA256(secret, "t=" + t + "." + raw_body) against the raw bytes of the request body (do not re-serialize the JSON).
  4. Constant-time compare expected against v1. Reject on mismatch.
Use the raw body
Re-serializing the JSON before hashing changes the byte sequence (key order, whitespace, number formatting) and will produce a different HMAC. Capture the raw body before your framework parses it — in Express, that's express.raw(); in FastAPI, that's await request.body().

Reference verifiers

import hashlib
import hmac
import time


def verify_webhook(secret: str, body: bytes, signature_header: str) -> bool:
    """Verify a Velora webhook delivery.

    Args:
        secret: the endpoint secret returned at registration time.
        body: the raw request body bytes (do NOT re-serialize the JSON).
        signature_header: the value of the X-Velora-Signature header.

    Returns:
        True on a valid signature within the 5-minute replay window.
    """
    parts = dict(p.split("=", 1) for p in signature_header.split(","))
    t = int(parts["t"])
    if abs(int(time.time()) - t) > 300:
        return False
    expected = hmac.new(
        secret.encode(),
        f"t={t}.".encode() + body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])

Retries

Velora retries non-2xx responses with exponential backoff and jitter. A delivery is considered terminal after the budget is exhausted.

  • Budget: 5 attempts.
  • Schedule: 1m → 5m → 30m → 2h → 12h, each shifted by ±20% jitter.
  • Considered success: any 2xx response within 10s.
  • Considered failure: non-2xx, connection error, TLS error, or timeout.

Auto-disable

An endpoint is auto-disabled after 10 consecutive failed deliveries (across all events, not per-event). Disabled endpoints stop receiving deliveries; the row stays in your list so you can inspect failure history. To resume, register a fresh endpoint with the same URL.

Idempotency
Retries replay the same payload, including the same id. Your handler should be idempotent on (id, type) so a re-delivered event doesn't double-count in your SIEM. Storing the last 5,000 ids in a LRUCache is usually enough.

List, disable, delete

GET    /api/v2/public/audit/webhooks
POST   /api/v2/public/audit/webhooks/{id}/disable
DELETE /api/v2/public/audit/webhooks/{id}

DELETE cascades to delivery history. POST /disable preserves history but stops dispatching new deliveries — the right call for short-term maintenance windows where you want the audit trail intact.

List endpoints

curl -H "Authorization: Bearer $VELORA_API_KEY" \
  https://api.velora.health/api/v2/public/audit/webhooks
{
  "endpoints": [
    {
      "id": "wh_8b2e5c11",
      "url": "https://your-app.example.com/velora-webhook",
      "event_filter": ["phi.", "admin."],
      "description": "Production SIEM",
      "active": true,
      "consecutive_failures": 0,
      "last_delivery_at": "2026-05-08T14:22:01+00:00",
      "created_at": "2026-05-08T14:22:01+00:00"
    }
  ]
}

Disable

curl -X POST -H "Authorization: Bearer $VELORA_API_KEY" \
  https://api.velora.health/api/v2/public/audit/webhooks/wh_8b2e5c11/disable

Delete

curl -X DELETE -H "Authorization: Bearer $VELORA_API_KEY" \
  https://api.velora.health/api/v2/public/audit/webhooks/wh_8b2e5c11

Operational checklist

Before pointing a production webhook at Velora:

  1. Capture the secret in a real secrets manager. Slack DMs and issue trackers are not real secrets managers.
  2. Wire signature verification before any business logic. Reject unsigned or stale-timestamp deliveries.
  3. Make your handler idempotent on (id, type).
  4. Acknowledge with a 2xx in under 10s. Defer slow work to a background queue — long-running handlers will time out and trigger retries.
  5. Alert on consecutive_failures > 3 from a poll of GET /webhooks — that gives you a window to fix before auto-disable trips.

Next

For runnable end-to-end webhook scripts (register, capture a delivery, verify, re-derive expected signature), see Code samples.