Implementing a receiving endpoint is covered in
Receiving Webhooks.
Event types
| Event | Fires when |
|---|
activity.deleted | An activity is deleted |
membership.joined | A user's membership in the organization starts |
membership.left | A holder leaves, is removed or revoked, or the organization is deleted |
membership.renewed | A membership renews |
membership.tier_changed | A membership moves to another tier |
reward.redeemed | A reward lands in a wallet that belongs to a Davi user |
webhook.test | A test delivery is sent from the dashboard |
reward.generate | Davi asks for a reward's content. Sent only to the webhook a reward names; see reward.generate |
No event fires when a membership's term runs out, whether it was not renewed or
it served out a cancellation. Access stops at valid_until.
membership.tier_changed fires for each membership moved, whether it moved on
its own, as part of moving every membership off a tier, or back to its previous
tier because the payment for a change was reversed.
reward.redeemed fires once the ledger transfer confirms, and only when the
receiving wallet is connected to a Davi user. A reward issued to a card or link
with no account behind it does not produce one.
Delivery envelope
Every delivery is a JSON body with the same outer shape.
| Field | Meaning |
|---|
event | The event type |
timestamp | When this attempt was sent, ISO 8601. A retry carries a later one |
organization_uuid | The organization that owns the webhook |
user_uuid | Always null for an organization webhook. The user an event concerns is in data |
data | The event's fields, below |
{
"event": "membership.joined",
"timestamp": "2026-01-15T18:42:07.512000+00:00",
"organization_uuid": "8f3c…",
"user_uuid": null,
"data": {
"origin": "user_app",
"schema_version": 1,
"user_uuid": "b21a…",
"organization_uuid": "8f3c…",
"tier_slug": "gold"
}
}
Event data
Every data object carries two common fields:
| Field | Meaning |
|---|
origin | user_app, or admin when Davi staff made the change |
schema_version | 1 |
The rest depends on the event. A field marked nullable can be null.
activity.deleted
| Field | Meaning |
|---|
activity_uuid | The deleted activity |
organization_uuid | Its organization |
activity_name | Its name at deletion |
deleted_by_user_uuid | Who deleted it. Nullable: null when no user did |
membership.joined
| Field | Meaning |
|---|
user_uuid | The holder |
organization_uuid | The organization |
tier_slug | The tier joined |
membership.left
| Field | Meaning |
|---|
user_uuid | The former holder |
organization_uuid | The organization |
ended_by | left (the holder's decision), removed (the organization's), or closed (the organization was deleted) |
membership.renewed
| Field | Meaning |
|---|
user_uuid | The holder |
organization_uuid | The organization |
membership.tier_changed
| Field | Meaning |
|---|
user_uuid | The holder |
organization_uuid | The organization |
tier_slug | The tier the membership is on now. Empty when a reversal returned it to a tier that has since been deleted |
reward.redeemed
| Field | Meaning |
|---|
wallet_address | The receiving wallet |
user_id | The recipient's user UUID |
username | Nullable |
first_name, last_name | Nullable |
account_type | The recipient's account type |
reward_template_id | The reward's UUID |
transaction_id | The ledger transaction that issued it |
organization_uuid | The reward's organization |
points | What the recipient was credited. Differs from the reward's points when their membership tier multiplies rewards. Nullable |
webhook.test
| Field | Meaning |
|---|
test | true |
message | A fixed description of the test |
timestamp | When the test was sent |
webhook.test has no origin or schema_version.
| Header | Value |
|---|
Content-Type | application/json |
X-Webhook-Event | The event type |
X-Webhook-Signature | sha256=<hex>, an HMAC-SHA256 of the raw body. The key is the SHA-256 hex digest of the signing secret, not the secret itself |
X-Webhook-Timestamp | When this attempt was sent, Unix seconds |
X-Webhook-Delivery | Unique delivery id, the same on every retry of that delivery |
Delivery states and retries
| Property | Value |
|---|
| Success | Any 2xx response |
| States | pending, success, failed, retrying |
| Timeout | 30 seconds per attempt |
| Retry schedule | Increasing backoff, approximately 0s, 1s, 5s, 30s, 2m and 10m after the initial attempt |
| Not retried | A 4xx other than 429. The delivery fails at once |
| Redirects | Not followed. A 3xx counts as a failed attempt |
| History | Status, response code, timings and error for each delivery, shown in the dashboard |
Endpoint requirements
| Requirement | Detail |
|---|
| URL | Must be publicly reachable. Private, loopback and .local addresses are rejected at creation |
| Signing secret | Returned once, when the webhook is created. Rotating it invalidates the previous secret |
reward.generate
A request for content, not a notification. When a reward whose content storage
is external is redeemed, Davi posts reward.generate to the webhook the reward
names in external_webhook_uuid, and nowhere else. Subscribing another webhook
to it delivers nothing. The named webhook must be enabled and belong to the
reward's organization.
The envelope, headers, signature, timeout and retries are the ones above. The
redemption answers the caller with a delivery_uuid straight away; poll
GET /api/v1/rewards/deliveries/{delivery_uuid} (scope reward:read) until its
status is completed or failed.
Request data
| Field | Meaning |
|---|
reward_template_id | The reward's UUID |
reward_template_name | Its name |
external_id | The reward's content_config.external_id. Nullable |
user_id | The recipient's user UUID |
username | Nullable: null for a wallet with no user behind it |
wallet_address | The receiving wallet |
transaction_id | Always "". The ledger transaction is created only after you answer |
The redemption's metadata is not forwarded. data carries no origin or
schema_version.
Response
Answer 2xx with a JSON body in one of two forms. A body in neither form, a
4xx, or running out of retries fails the redemption and returns its reserved
supply.
Inline content. Davi stores the content at issuance and serves it from then
on. It never changes afterwards.
{
"content": {
"content_version": "1.0",
"generated_at": "2026-07-22T10:30:00Z",
"data": {},
"items": [
{
"type": "ticket",
"title": "Main Stage Pass",
"ticket_number": "TICKET-2026-000123",
"starts_at": "2026-09-15T09:00:00Z",
"barcode_value": "TCKT-8F3A-19C2-77BE",
"barcode_format": "qrcode"
}
]
}
}
A hosted manifest. Davi fetches the content from you when the holder opens
the reward.
{
"manifest_url": "https://partner.example.com/davi/manifests/abc123",
"access_key": "a-bearer-token-for-this-manifest"
}
| Rule | Detail |
|---|
| Fetch | GET manifest_url with Authorization: Bearer <access_key>, answered with the same object as content above |
manifest_url | An absolute http(s) URL on a public host |
| Timeout | 10 seconds per fetch |
| Caching | Davi caches the manifest and fetches it again when the cache expires, so it must keep serving the same content for the reward's lifetime |
Content fields
| Field | Meaning |
|---|
content_version | Required. A string you choose, such as "1.0" |
data | Required. A free-form object |
items | The reward's items. Each has a type: attachment, certificate, badge, coupon, voucher, ticket or asset |
generated_at | ISO 8601. When the content was produced |
Each item type's fields are listed in the API Reference with the
reward schemas. A ticket requires type, title and ticket_number.