Events and member synchronization.
The full 32-event catalog, grouped by kind, plus how to keep your own member records in sync.
An event is one thing that happened in Reward Loyalty: a member earned points, a stamp card filled up, a voucher got used. You choose which events an endpoint receives when you add it; this page lists everything on offer, what each one carries, and how to use the member data every event carries to keep your own records current.
The event catalog
You can subscribe an endpoint to any of 32 events, organized in seven groups. The badge on each event tells you what kind of fact it reports:
| Badge | Meaning |
|---|---|
| Broad | Covers every change of one kind, such as any points increase. Good for balance mirrors. |
| Detailed | One specific committed action, such as a purchase or a void. Good for automation. |
| Lifecycle | The platform commits the change itself, from a schedule or a rule: expiry sweeps, tier movement, automatic voucher gifts. |
| Privacy | A deletion notice, with stricter payload retention. |
A ping event also exists, sent only by the Send ping button. It is a connection test, never a subscription choice.
Points and rewards
Purchases, point changes, and reward redemptions.
| Event | Key | Badge | Sent when |
|---|---|---|---|
| A member earns points on a purchase. | points.earned |
Broad | A member's point balance increases, such as a purchase, a bonus, or a received transfer. |
| Points are deducted from a member, such as a redemption. | points.spent |
Broad | A member's point balance decreases, such as a reward redemption, a sent transfer, or a correction. |
| A purchase is recorded for a member. | purchase.recorded |
Detailed | One accepted purchase is recorded, at the counter, through the API, or by a connected store. Never sent for points-only credits. |
| A member's points are adjusted manually. | points.adjusted |
Detailed | Staff or an API credits or deducts points directly, outside purchases, rewards, and transfers. |
| Awarded points are reversed. | points.reversed |
Detailed | A void or a refund removes previously awarded points from a member. |
| Points expire on a member's card. | points.expired |
Lifecycle | The daily sweep expires a member's unused points on one card. |
| A member transfers points to another member. | points.transferred |
Detailed | Once per transfer, with both the sender and the receiver. The matching balance events are sent separately. |
| A member redeems a points code. | points.code_redeemed |
Detailed | A member redeems a points code and the points are credited. The code value itself is never included. |
| A member redeems a reward with points. | reward.redeemed |
Detailed | A points reward redemption is recorded for a member. |
Stamps
Stamps earned, corrections, and stamp card rewards.
| Event | Key | Badge | Sent when |
|---|---|---|---|
| A stamp is added to a stamp card. | stamp.added |
Detailed | A member earns one or more stamps on a stamp card. |
| A member completes a stamp card. | stamp_card.completed |
Detailed | A member fills the last slot of a stamp card. |
| A member redeems a stamp card reward. | stamp.reward_redeemed |
Detailed | A member redeems a completed stamp card reward. |
| A member's stamps are adjusted manually. | stamp.adjusted |
Detailed | Staff corrects a member's stamp count directly, up or down. |
| A stamp transaction is voided. | stamp.voided |
Detailed | A previous stamp transaction is reversed by a correction. |
| Stamps expire on a member's stamp card. | stamp.expired |
Lifecycle | The daily process expires a member's unused stamps on one stamp card. |
Vouchers
Vouchers claimed, issued, redeemed, and voided.
| Event | Key | Badge | Sent when |
|---|---|---|---|
| A member redeems a voucher. | voucher.redeemed |
Detailed | A voucher redemption is recorded for a member. |
| A member adds a voucher to their wallet. | voucher.claimed |
Detailed | A member adds an existing voucher to their wallet, through a claim link, a save action, or a wallet pass. |
| A voucher is issued to a member automatically. | voucher.issued |
Lifecycle | A voucher is created or assigned for one member automatically, such as a tier reward or a birthday gift. |
| A voucher redemption is voided. | voucher.redemption_voided |
Detailed | A recorded voucher redemption is voided and the use is restored. |
| A voucher reaches its total usage limit. | voucher.exhausted |
Lifecycle | A voucher reaches its total usage limit and can no longer be redeemed. |
Passes
Prepaid passes issued, used, and expired.
| Event | Key | Badge | Sent when |
|---|---|---|---|
| A prepaid pass is issued to a member. | pass.issued |
Detailed | A prepaid pass is issued to a member. |
| A member uses a visit on a prepaid pass. | pass.used |
Detailed | One or more visits are deducted from a member's prepaid pass. |
| A prepaid pass visit is voided. | pass.use_voided |
Detailed | A recorded visit is voided and returned to a member's prepaid pass. |
| A prepaid pass is fully used. | pass.used_up |
Lifecycle | The final visit of a prepaid pass is used. |
| A prepaid pass expires. | pass.expired |
Lifecycle | The daily process closes a prepaid pass whose validity period has ended. |
Members and tiers
Members joining, leaving, and changing tiers.
| Event | Key | Badge | Sent when |
|---|---|---|---|
| A member enrolls in a card or stamp card. | member.enrolled |
Detailed | A member joins a card or stamp card for the first time. |
| A member leaves a card or stamp card. | member.unenrolled |
Detailed | A member removes a card or stamp card from their wallet. Never sent for privacy deletions. |
| A member's tier changes. | member.tier_changed |
Lifecycle | A member receives a first tier, an upgrade, or a downgrade. Contains tier IDs and levels, never tier names. |
See Member synchronization below for how these three fit together with the rest of the catalog.
Member cards
Physical member cards you issue: created, claimed, and disabled.
| Event | Key | Badge | Sent when |
|---|---|---|---|
| A physical member card is issued. | member_card.issued |
Detailed | A physical card you issued is created, alone or as part of a batch. The card number is never included. |
| A member claims a physical member card. | member_card.claimed |
Detailed | A member links a physical card you issued to their account. The card number is never included. |
| A physical member card is disabled. | member_card.disabled |
Detailed | A physical card you issued is disabled. The reason is a fixed code; the card number is never included. |
These events cover cards issued from your own card studio. Cards issued by the platform operator or created by members themselves send no webhook, because no single business owns those.
Privacy
Deletion notices, so your systems can remove their copy too.
| Event | Key | Badge | Sent when |
|---|---|---|---|
| A member deletes their data for your business. | member.relationship_removed |
Privacy | A member's data for your business has been deleted, so your systems can delete their copy too. |
See Personal data and deletion notices for what this event obliges your systems to do.
Member synchronization
Most integrations built on these events need their own copy of who a member is. Reward Loyalty gives you two tools for that: an explicit enrollment event for the moment someone joins a card, and a member fragment carried on nearly every other event, so you never have to wait for enrollment to hear about someone new.
member.enrolled fires the first time a member is explicitly enrolled on a card or stamp card: when the member joins it themselves (scanning a QR code, or following from their wallet), when staff credit a first purchase or add a first stamp on a card the member had not joined, or when a member adds a stamp card to Google Wallet before joining. It fires at most once per member per card, for a genuinely new attachment only, never for a member who already joined.
Staff crediting a first purchase does enroll the member and does raise member.enrolled. Within that one visit, the events are created in this order: the purchase is recorded first, raising purchase.recorded and points.earned, and the enrollment check runs afterward, raising member.enrolled. That does not make member.enrolled a guaranteed second event to wait for: delivery order is not guaranteed (see below), so build your integration to upsert from whichever event arrives, not to depend on member.enrolled following or leading any other event. A few other attachments happen as a side effect of another action and never raise a separate member.enrolled at all: adding a loyalty card to Google Wallet, a WooCommerce or Shopify order, an Agent or REST API follow, or a voucher bonus. Each of those surfaces as its own event instead, for example points.earned or stamp.added. To catch every attachment, subscribe to those events as well, not member.enrolled alone.
Do not use points.earned to detect a new member. It fires on every purchase that earns points, not only the first, so it tells you a balance changed, never that someone is new.
The two membership events are not symmetric. member.unenrolled fires every time a member removes the card from their wallet, while member.enrolled fires at most once per member per card. A member who leaves and later re-adds the same card raises no second member.enrolled, so over time your system can receive two member.unenrolled deliveries against one member.enrolled. Treat member.unenrolled as the current state, never as a counter to balance.
Nearly every event carries a member fragment, not just member.enrolled. Delivery order across different event types is not guaranteed, even within one operation: the example above shows a genuine enrollment landing after the purchase that triggered it, and a member's first delivery to reach you could just as easily be points.earned, voucher.claimed, or any other event that happens to fire first. Build your sync to upsert (create-or-update) your own copy of the member record from the member fragment of whichever event arrives first, keyed on member.id, rather than waiting for member.enrolled or assuming it leads. member.enrolled stays useful for what it actually signals, an explicit loyalty or stamp card enrollment, not a universal "member created" event, and some members will never trigger one at all.
Choosing events for your program type
The event picker only offers groups and events your business can currently use. A stamp-only business never sees the Passes group, for example, because there is nothing there to subscribe to:
| Events | Requires |
|---|---|
Points and rewards group, and member.tier_changed |
Loyalty cards |
| Stamps group | Stamp cards |
| Vouchers group | Vouchers |
| Passes group | Prepaid passes |
| Member cards group | Member cards (your own card studio) |
member.enrolled, member.unenrolled |
Loyalty cards or stamp cards, either is enough |
Privacy group (member.relationship_removed) |
Always available |
If you subscribe an event while your business has the matching card type, then later stop using that card type, the subscription does not silently disappear. It stays active on that endpoint, still delivering, with a quiet Not in your plan badge in the picker so you remember why it is there. It stops only when you remove it yourself. A brand new endpoint can never start with an event outside what your business currently uses, and an existing endpoint can never gain a new one it did not already have.
Broad and detailed events together
One action at the counter can commit more than one loyalty fact. The broad events, points.earned and points.spent, report every balance change of their kind; the detailed events name the specific action. Selecting both is valid and common: the broad event keeps a balance mirror simple, the detailed event drives automation. Nothing is suppressed; each subscribed event arrives as its own delivery.
| One operation | Events that may be sent |
|---|---|
| Purchase with points | purchase.recorded, points.earned; an enrollment bonus can add a second points.earned in the same operation |
| Staff void or store refund | points.reversed, points.spent |
| Manual or API points-only adjustment | points.adjusted, then points.earned or points.spent depending on the direction |
| Point transfer | One points.transferred, one sender points.spent, one receiver points.earned |
| Points code redemption | points.code_redeemed, points.earned |
| Points reward redemption | reward.redeemed, points.spent |
| Final voucher use | voucher.redeemed, then voucher.exhausted if the total limit is reached |
| Final pass use | pass.used, then pass.used_up |
| Stamp card completion with a points reward | stamp.added, stamp_card.completed, and the resulting points.earned |
Every event from one operation shares one correlation_id. Each event has its own event_id, and each delivery to each endpoint has its own id. Here is one purchase arriving as two correlated deliveries:
{
"id": "0199b7c2-4d5e-7f60-8a1b-2c3d4e5f6a7b",
"event_id": "7d3f2a68-9c1e-4b5a-8f2d-0e6a4c8b2d19",
"event": "purchase.recorded",
"schema_version": 1,
"created_at": "2026-07-10T14:32:05+00:00",
"occurred_at": "2026-07-10T14:32:03Z",
"partner_id": "b4a4e2b0-8f4e-4d0a-9a3f-1c2d3e4f5a6b",
"correlation_id": "3f8c9d0e-1a2b-4c3d-8e5f-6a7b8c9d0e1f",
"test": false,
"data": {
"member": {
"id": "ee7f0d16-b176-44c5-9dd2-ee99bbc433e4",
"unique_identifier": "344-319-665-971"
},
"card_id": "5b8d32c2-2b04-4033-a2dc-f750d762de88",
"transaction_id": "e855986e-744c-4418-a4b8-6afbf7c04323",
"purchase_amount_minor": 1250,
"currency": "EUR",
"points_awarded": 50,
"source": "staff",
"business_time": {
"timezone": "Europe/Amsterdam",
"local_date": "2026-07-10"
}
}
}
{
"id": "0199b7c2-4d5e-7f60-8a1b-5e6f7a8b9c0d",
"event_id": "9a5b3c71-2d4e-4f6a-9b8c-1d0e2f3a4b5c",
"event": "points.earned",
"schema_version": 1,
"created_at": "2026-07-10T14:32:05+00:00",
"occurred_at": "2026-07-10T14:32:03Z",
"partner_id": "b4a4e2b0-8f4e-4d0a-9a3f-1c2d3e4f5a6b",
"correlation_id": "3f8c9d0e-1a2b-4c3d-8e5f-6a7b8c9d0e1f",
"test": false,
"data": {
"member": {
"id": "ee7f0d16-b176-44c5-9dd2-ee99bbc433e4",
"unique_identifier": "344-319-665-971"
},
"card_id": "5b8d32c2-2b04-4033-a2dc-f750d762de88",
"points": 50,
"purchase_amount": 1250,
"transaction_id": "e855986e-744c-4418-a4b8-6afbf7c04323",
"ledger_event": "staff_credited_points_for_purchase",
"source": "staff",
"balance_after": 150,
"purchase_amount_minor": 1250,
"currency": "EUR"
}
}
Both carry the same correlation_id and the same transaction_id, so a consumer that receives both knows they describe one sale.
The envelope and headers
Every delivery is a POST request whose JSON body wraps the event data in a fixed envelope:
| Field | Meaning |
|---|---|
id |
The delivery id, unique per delivery to one endpoint and stable across retries. The same value rides in the X-RewardLoyalty-Delivery header. Use it to skip duplicates. |
event_id |
The business fact's id. Every endpoint receiving the same fact gets the same event_id. Use it to correlate across endpoints. |
event |
The event key, for example purchase.recorded. |
schema_version |
The payload schema version, 1 today. Fields are only added, never removed or renamed; a change that breaks that promise would raise this number. |
created_at |
When the delivery record was created, ISO 8601 with a numeric UTC offset, for example 2026-07-10T14:32:05+00:00. This field predates the rest of the envelope and keeps its original format so older consumers keep parsing it. |
occurred_at |
When the loyalty fact was committed, UTC with a trailing Z, for example 2026-07-10T14:32:03Z. Use this one for timelines and ordering. |
partner_id |
Your partner id. |
correlation_id |
Shared by every event from one operation. null when no useful correlation exists. |
test |
true only for a ping or a sample send, never for a production event. |
data |
The event's own fields, described per event below. |
Six headers ride along with every request:
| Header | Value |
|---|---|
Content-Type |
application/json |
X-RewardLoyalty-Event |
The event key, for example points.earned |
X-RewardLoyalty-Delivery |
The delivery id, identical to id in the body |
X-RewardLoyalty-Event-Id |
The business fact's id, identical to event_id in the body |
X-RewardLoyalty-Schema-Version |
The payload schema version, identical to schema_version in the body |
X-RewardLoyalty-Signature |
sha256= followed by the base64-encoded HMAC-SHA256 of the raw request body, signed with the endpoint's own secret. See Security and signature verification. |
The headers mirror body fields for convenience; the signed JSON body is the authoritative copy.
A field declared for an event is always present. When a value does not apply, or an older record never stored it, the field is JSON null; a declared field never disappears from the payload.
The member object
Every member field (and the sender and receiver fields on a transfer) carries exactly two values:
{ "id": "ee7f0d16-b176-44c5-9dd2-ee99bbc433e4", "unique_identifier": "344-319-665-971" }
id is the member's internal id and unique_identifier the stable code staff and APIs use to find them. No name, email address, or other profile detail rides in a webhook payload. Look a member up through the REST API or the Agent API when a delivery needs to show more; APIs write, webhooks notify.
Money fields
Money in a webhook is always a pair: an integer field ending in _minor and an uppercase ISO 4217 currency code. The integer counts the currency's smallest unit, so the number of decimal places depends on the currency, never on the payload:
| Payload | Currency rule | Amount |
|---|---|---|
"purchase_amount_minor": 1250, "currency": "EUR" |
Euro has 2 fraction digits | 12.50 EUR |
"purchase_amount_minor": 1250, "currency": "JPY" |
Yen has 0 fraction digits | 1250 JPY |
"purchase_amount_minor": 1250, "currency": "BHD" |
Bahraini dinar has 3 fraction digits | 1.250 BHD |
Never assume two fraction digits and never parse a formatted string; look up the fraction digits for the delivered currency code. Point counts, stamp counts, and visit counts are plain integers. A signed change ends in _delta; a named quantity such as points_expired is never negative.
The older purchase_amount field on points.earned and points.spent also carries integer minor units and stays for backward compatibility. Prefer purchase_amount_minor with currency in new integrations.
Timestamps and business time
Every exact instant in a payload is a UTC ISO 8601 string with a trailing Z, such as expired_at, claimed_at, or the envelope's occurred_at. Store and compare those as they are.
Events driven by a calendar rule (a purchase schedule, an expiry, a validity window, a birthday gift) also carry a business_time object:
"business_time": {
"timezone": "Europe/Amsterdam",
"local_date": "2026-07-10"
}
timezone is the owning business's IANA time zone and local_date the calendar date of the event in that zone. This answers "which business day was that?" without you re-implementing time zone math, including across daylight saving changes. Two examples on the night Amsterdam leaves summer time (October 25, 2026):
occurred_at: 2026-10-24T22:30:00Zis 00:30 local, solocal_dateis2026-10-25while the UTC date still reads October 24.occurred_at: 2026-10-25T01:30:00Zis 02:30 local, after the clocks fell back, andlocal_dateis again2026-10-25.
That local day is 25 hours long, and both deliveries land on it. Group reports by local_date when you want business days; order and deduplicate by occurred_at, which never repeats or jumps.
Event payloads
Every event's exact fields are listed in the Webhook event field reference, one table per event with a generated example for each group. Turn there when you need the precise shape of an event's data block; the catalog above tells you which events exist and when they fire.
Related topics
- Webhook event field reference: Every field in all 32 events, with a generated example per group
- Webhooks overview and setup: Turn on Webhooks and add your first endpoint
- Security and signature verification: What the signing secret protects, and how to check it in your own code
- Deliveries, retries, and troubleshooting: Test sends, retries, auto-pause, and duplicates
- REST API overview: Look up a member's full profile after a webhook arrives
- Agent API overview: Build your own automation against the same platform