POST carrying a JSON object with two keys. This
page documents that envelope, then the fields inside each of the five object families.
For what causes each event to fire, see Event types.
The HTTP request
The body is always UTF-8 JSON. Verify the signature against the raw bytes before parsing — see
Verify signatures.
The envelope
The event type that fired, always
family.action — for example sale.created. Identical to the
Svix event type on the message.The object the event happened to, with its useful relations expanded. For
deleted events this
contains only id.data, no previous_attributes, and no envelope-level timestamp — use svix-timestamp
or the object’s own createdAt.
Field conventions
These hold across every family.Enum values are uppercase
Enums serialise exactly as stored:"PURCHASE", "ACTIVE", "STRIPE", "LINK". Compare
case-sensitively against the uppercase form.
Dates are ISO 8601 strings
createdAt, updatedAt, expiresAt, refundedAt, and friends are ISO 8601 with a timezone, for
example "2026-03-11T09:24:18.442Z". Never a Unix timestamp.
IDs are UUID strings, except sales
Affiliates, referrals, coupons, and promotional codes use UUID strings. Sales use an auto-incrementing integer, sobody.id on a sale event is a number, not a string, and so is the
id on sale.deleted. Type your handler accordingly.
Money is a plain number in the program’s currency
Amounts such astotalEarned, commissionEarned, and totalRevenue are decimal numbers in your
program’s currency — 199.99, not 19999 minor units. There is no per-payload currency field on
sales or referrals; the currency is a property of the program.
Assume nullable
Most fields other thanid are nullable. email, name, affiliateId, referralId,
commissionRate, and every relation can be null. Code defensively rather than trusting a field to
be present because it was present last time.
Sale payloads
The largest and most commonly consumed payload. Emitted onsale.created and sale.updated.
commissionEarned versus commissions
A single sale can generate several commissions — the selling affiliate’s, plus a parent’s under multi-level rules, plus bonuses. Thecommissions array holds all of them.
The top-level commissionEarned is a convenience: it is the commissionEarned of the commission
belonging to this sale’s own affiliate. If you only pay the direct affiliate, read the top-level
field. If you mirror the full ledger, iterate commissions.
Each entry in commissions carries id, affiliateId, commissionEarned, approvalStatus
(ON_HOLD, ACCEPTED, or DECLINED), rewardType (CASH or NON_CASH), payoutCreated,
payoutId, manuallyPaid, holdReason, bonusReason, createdAt, and updatedAt.
Example: sale.created
Referral payloads
Emitted onreferral.created and referral.updated.
status is an alias
The stored column issubscriptionStatus. Referly copies it to status in the webhook payload for
convenience, so both keys appear with the same value. Read either — but be aware status is a
webhook-only convenience and is not a field on the referral elsewhere in the API.
Example: referral.created
Affiliate payloads
Emitted onaffiliate.created and affiliate.updated. The affiliate is a user record, so this is
the widest payload — the fields below are the ones you can rely on.
link versus affiliateLinks
In webhook payloads,link is populated with the slug of the affiliate’s first referral link as
a convenience. That differs from the REST API, where the affiliate object’s link property is
deprecated and usually null.
An affiliate can own many slugs, so link is lossy. Read affiliateLinks whenever an affiliate might
have more than one — see
affiliates and referral links.
Example: affiliate.created
Coupon payloads
Emitted oncoupon.created and coupon.updated. The body is the coupon with its
promotionalCodes array expanded.
Promo code payloads
Emitted onpromocode.created and promocode.updated. The body is the promotional code with its
parent coupon expanded.
Delete events
All fivedeleted events share one shape. The object is gone, so there is nothing to expand:
id of everything you process. On a sale.deleted you get
the integer and nothing else — no affiliate, no amount, no customer — so your own records are the
only way to know what to reverse.
Payloads are snapshots
The body is serialised once, when the event is sent. It is not re-read on delivery. That has two consequences:- A retry delivers the original payload. If a delivery fails at 09:00 and succeeds on retry at 14:00, you receive the 09:00 state, not the current state. See Retries and logs.
- Events can arrive stale or out of order. A
sale.updatedthat succeeded on first try can land before asale.createdthat needed three attempts. Where ordering matters, comparecreatedAtor re-read the object through the REST API before acting on it.
Fields not listed here
Payloads are built by spreading the stored record, so a delivery may contain fields beyond this reference — internal columns, newly added fields, relations included for other reasons. Treat anything not documented here as unstable: safe to log, not safe to depend on. Equally, do not assume a documented field is always present — parse defensively and ignore what you do not recognise, so a new field never breaks your handler.Related
Event types
Which events produce which of these bodies.
Verify signatures
Check the signature before you parse the body.
Retries and logs
Inspect the exact payload that was delivered.
Data model
How these objects relate to each other.
Statuses and enums
Every enum value you can receive.
Test and replay
Send yourself a real payload to inspect.