Documentation

Webhooks

Receive real-time events and verify their signatures.

Webhooks push events to your server as they happen, so you don't have to poll. A webhook belongs to an organization.

Configuration is a dashboard action. Webhooks are created and managed in the Davi dashboard, not through the public API — the management scopes are first-party only, so a third-party token can't obtain them. This is deliberate: a webhook records no creating app, so two integrations on the same organization would otherwise overwrite each other's subscriptions. Per-app subscriptions are planned. What this page documents is the part you implement: receiving and verifying deliveries.

Event types

| Event | Fires when | |---|---| | activity.registered | A user registers for an activity | | activity.checked_in | A user checks in / attends | | activity.cancelled | A registration is cancelled | | activity.deleted | An activity is deleted | | membership.joined | A user joins an organization | | membership.left | A user leaves an organization | | membership.renewed | A membership renews | | membership.expiring | A membership is about to expire | | reward.redeemed | A reward is redeemed |

A webhook.test event is sent when you trigger a test delivery.

Subscribing

Create a webhook for your organization with the endpoint URL and the events you want. The URL must be publicly reachable — private, loopback, and .local addresses are rejected. On creation the signing secret is returned once; store it securely (you can rotate it later).

Payload

Each delivery is a JSON body with a consistent envelope. event and timestamp identify the event; organization_uuid / user_uuid give its context; and data carries the event-specific fields (its shape depends on the event type):

{
  "event": "activity.checked_in",
  "timestamp": "2026-01-15T18:42:07Z",
  "organization_uuid": "8f3c…",
  "user_uuid": "b21a…",
  "data": {
    "wallet_address": "0x…",
    "user_id": "b21a…",
    "activity_id": "6d4e…",
    "session_id": "1c7f…"
  }
}

Each delivery also carries these headers:

| Header | Value | |---|---| | X-Webhook-Event | The event type | | X-Webhook-Signature | sha256=<hex> HMAC signature of the raw body | | X-Webhook-Timestamp | Unix timestamp (seconds) | | X-Webhook-Delivery | Unique delivery ID |

Verifying signatures

Every delivery is signed with HMAC-SHA256 over the raw request body using your webhook secret. Always verify before trusting a payload, and compare in constant time.

import hashlib
import hmac

def verify(secret: str, raw_body: bytes, signature_header: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature_header)
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(secret, rawBody, signatureHeader) {
  const expected =
    "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signatureHeader);
  return a.length === b.length && timingSafeEqual(a, b);
}

Compute the signature over the exact bytes you received — don't re-serialize the JSON first, or the signature won't match.

Responding, retries, and deliveries

  • Respond with a 2xx status quickly (deliveries time out after 30 seconds). Do heavy work asynchronously.
  • Failed deliveries are retried with increasing backoff (roughly at 0s, 1s, 5s, 30s, 2m, and 10m). A delivery is pending, success, failed, or retrying.
  • You can inspect delivery history — status, response code, timings, and error — and send a test event from the dashboard.

Next