Loyalty
This page is the API reference for developers reading or writing loyalty programmes, members, rewards, referrals or company bonus agreements.
The Loyalty app holds the loyalty offers of a shop. A points programme gives points to contacts or companies for each credited order, moves them up tiers by the points they earned in the period, and lets them exchange points for single-use reward vouchers. A turnover bonus programme (B2B) holds one bonus agreement per company and year. Its tiers are measured against a base turnover, by default last year's final turnover. The bonus is calculated after the year has closed and paid once it is approved. Approval asks that receivables are confirmed settled, unless the tenant has switched that check off (it is on by default). Referral codes reward the new customer with a welcome voucher (tenant setting referee_reward_promotion_id, issued at the claim) and the member who referred them with a voucher (referral_reward_promotion_id) once the referee's first credited order qualifies the referral.
The app never discounts by itself. To give a tier a perk such as free shipping, create a promotion with a loyalty_tier condition. Rewards issue their vouchers through promotions too.
Base path: https://api.revenexx.com/v1/loyalty.
Objects
| Object | What it holds |
|---|---|
| Programme | code, labels, kind (points or turnover bonus), holder (contact or organisation), period, currency, auto_enroll and active. |
| Tier | The programme it belongs to, code, labels, position, the threshold (threshold_kind and threshold_value), bonus_percent, points_multiplier and perks. |
| Reward | code, points_cost, the promotion_id whose vouchers it issues, min_tier_id and active. |
| Membership | One contact or company in one programme: its tier, status, and balances of points (period, lifetime and current). |
| Point booking | One entry of the point ledger: kind, points, remaining, the order, reward or redemption it came from, and reason. |
| Reward redemption | A reward redeemed by a member, with its voucher_code, the points it cost and its state. |
| Referral code | A member's code, its uses and whether it is active. |
| Referral | Who referred whom, its state, the order, the reason and the voucher codes issued to referee and referrer. |
| Bonus agreement | One company and year: the programme, the base turnover, the turnover so far, the projection, the reached tier, the bonus and its settlement state. |
| Turnover entry | One line of an agreement: an order, a return, an ERP figure or a correction. |
List operations take limit (1 to 200, default 50) and offset. The programme, tier and reward lists also take trashed.
Scopes
A key calls these routes with the capability key of each route. The App's manifest names one per operation, in the form loyalty.<group>.<operation>, for example loyalty.programs.list. The API keys page explains that a key holds capability keys or a namespace such as loyalty.*. The scope of each operation is listed in the tables below. A call that needs a scope the key does not hold is refused with 403.
Backdating
With the tenant setting allow_backdating switched on (off by default, meant for test tenants), a write may carry at, an ISO date-time in the past. It may not be in the future, may be at most 24 months back, and needs a time zone. The hourly job, the nightly trash purge and a turnover correction keep their own time. A moment that cannot be honoured is refused with 400 and backdating_invalid.
Trash
The shared rules for the trash, including the difference between emptying it and the purge, are on Trash. Programmes, tiers and rewards carry the six trash fields.
DELETE on a programme, tier or reward moves it into the trash. Its restore route takes it out again. A trashed item is deleted for good by the nightly purge once the trash retention has passed, which is 90 days unless the tenant sets another value. A second DELETE and trash/empty delete for good at once; rows that are still blocked stay in the trash, and trash/empty lists them in blocked (at most 1000 rows per call). trash/purge deletes only what has been in the trash longer than the retention.
A first DELETE is refused with 409 and trash_blocked, a second DELETE with 409 and delete_blocked. The reason is in details.blockers[].code:
has_members: a programme that still has members. At the firstDELETE, a programme with bonus agreements also answershas_members.has_agreements: a trashed programme that still has bonus agreements, at the secondDELETE(delete for good).tier_in_use: a tier that an active member holds.redeemed: a reward that has a redemption.
Nothing is switched off automatically. Set active to false with PUT instead. A storefront contact gets 403 for deleting for good, and for trash/empty and trash/purge (purge_not_allowed).
Idempotency keys and sync fields
keyis an idempotency key on redeem, adjust, award and turnover entries. It is unique per tenant per operation: award, adjust, redeem and turnover entries each have their own key space, so the same key may be used across operations. The first call with a key answers201andcreated: true. A repeat of the same key answers200andcreated: falseand books nothing new. Reusing a key for another member or another reward books nothing and answers the first entry, so use a new key for each booking. Without akey, every call books again.- Sync fields are
external_id,external_refs,source_dataandsource_synced_at: the record's id in an outside system, its references there, and the data it was synced from and when. Programmes, tiers and rewards take them, optionally, on create and update. Memberships and bonus agreements store them too, and their lists filter onexternal_id, but no route writes them (as of 2026-10): the membership create and update calls accept them in the body and ignore them. See Sync fields for what each field means.
Programmes
| Operation | Scope | What it does |
|---|---|---|
GET /v1/loyalty/programs | loyalty.programs.list | Lists the programmes of the tenant. |
POST /v1/loyalty/programs | loyalty.programs.create | Creates a programme. |
GET /v1/loyalty/programs/{id} | loyalty.programs.get | Reads one programme. |
PUT /v1/loyalty/programs/{id} | loyalty.programs.update | Corrects a programme. |
DELETE /v1/loyalty/programs/{id} | loyalty.programs.delete | Moves a programme into the trash, with its tiers and rewards. |
POST /v1/loyalty/programs/{id}/restore | loyalty.programs.restore | Brings a programme back out of the trash. |
POST /v1/loyalty/programs/trash/empty | loyalty.programs.empty-trash | Deletes every programme in the trash for good. |
POST /v1/loyalty/programs/trash/purge | loyalty.programs.purge | Deletes the programmes that outlived the trash retention. It is the nightly purge, callable by hand. |
GET /v1/loyalty/programs/{id}/figures | loyalty.programs.figures | Reads the active members of a programme per tier. |
POST /v1/loyalty/programs/{id}/open-period | loyalty.programs.open-period | Opens the yearly bonus agreements for every member of a turnover bonus programme. |
GET /v1/loyalty/programsfilters:kind(for exampleturnover_bonus),active,holder,currency,period,auto_enroll,member_count,external_idandcode, plustrashed.POST /v1/loyalty/programsbody: requiredcode,labelsandkind, plusholderfor a points programme. Optional:period,currency,auto_enroll,active,description,metadataand the sync fields. A turnover bonus programme takesholder: organizationandperiod: calendar_yearby default. A programme created with theX-Revenexx-Marketheader belongs to that market; without it, the programme applies in every market.PUT /v1/loyalty/programs/{id}errors:400withkind_fixedwhen the kind or the holder is changed.POST /v1/loyalty/programserrors:409withcode_takenwhen the code is in use, and409within_trashwhen a trashed programme has that code.open-periodbody:period, a four-digit year (default the current year). A points programme is refused with400not_turnover_programme, and a bad year with400invalid_period.
Tiers
| Operation | Scope | What it does |
|---|---|---|
GET /v1/loyalty/tiers | loyalty.tiers.list | Lists the tiers. |
POST /v1/loyalty/tiers | loyalty.tiers.create | Adds a tier to a programme. |
GET /v1/loyalty/tiers/{id} | loyalty.tiers.get | Reads one tier. |
PUT /v1/loyalty/tiers/{id} | loyalty.tiers.update | Corrects a tier. |
DELETE /v1/loyalty/tiers/{id} | loyalty.tiers.delete | Moves a tier into the trash. |
POST /v1/loyalty/tiers/{id}/restore | loyalty.tiers.restore | Brings a tier back out of the trash. |
POST /v1/loyalty/tiers/trash/empty | loyalty.tiers.empty-trash | Deletes every tier in the trash for good. |
POST /v1/loyalty/tiers/trash/purge | loyalty.tiers.purge | Deletes the tiers that outlived the trash retention. |
POST /v1/loyalty/tiersbody: requiredprogram_id,codeandlabels. Optional:position,threshold_kind,threshold_value,bonus_percent,points_multiplier,perks,active,metadataand the sync fields.- Errors:
409within_trashwhen the programme is in the trash.400for a value out of range (bonus_percentfrom 0 to 100,threshold_value0 or more,points_multiplierabove 0), or athreshold_kindofrelative_to_baseon a points programme.409withcode_takenfor a code in use.
Rewards
| Operation | Scope | What it does |
|---|---|---|
GET /v1/loyalty/rewards | loyalty.rewards.list | Lists the rewards. |
POST /v1/loyalty/rewards | loyalty.rewards.create | Adds a reward to a points programme. |
GET /v1/loyalty/rewards/{id} | loyalty.rewards.get | Reads one reward. |
PUT /v1/loyalty/rewards/{id} | loyalty.rewards.update | Corrects a reward. |
DELETE /v1/loyalty/rewards/{id} | loyalty.rewards.delete | Moves a reward into the trash. |
POST /v1/loyalty/rewards/{id}/restore | loyalty.rewards.restore | Brings a reward back out of the trash. |
POST /v1/loyalty/rewards/trash/empty | loyalty.rewards.empty-trash | Deletes every reward in the trash for good. |
POST /v1/loyalty/rewards/trash/purge | loyalty.rewards.purge | Deletes the rewards that outlived the trash retention. |
POST /v1/loyalty/rewards/{id}/redeem | loyalty.rewards.redeem | Redeems points for a single-use reward voucher. |
POST /v1/loyalty/rewardsbody: requiredprogram_id,code,labels,points_costandpromotion_id. Optional:description,min_tier_id,voucher_batch_id,active,metadataand the sync fields.POST .../redeembody: the member is named bymembership_id, or bycontact_idororganization_id. Optional:key.- Redeem errors:
409withinsufficient_points,tier_too_low(the member's tier is below the reward'smin_tier_id),reward_inactive,membership_inactiveorprogram_inactive.400withwrong_programme, orcontact_requiredwhen a company membership redeems withoutcontact_id, the person the voucher is issued to.404withno_membershipwhen no membership of the reward's programme is found.502withvoucher_unavailablewhen the promotions app does not issue a code.
Reward redemptions
| Operation | Scope | What it does |
|---|---|---|
GET /v1/loyalty/reward-redemptions | loyalty.rewards.redemptions-list | Lists the redeemed rewards. |
GET /v1/loyalty/reward-redemptions/{id} | loyalty.rewards.redemptions-get | Reads one redeemed reward. |
- Filters:
membership_id,reward_id,voucher_code,state,request_keyandused_order_id, pluspoints.
Memberships
| Operation | Scope | What it does |
|---|---|---|
GET /v1/loyalty/memberships | loyalty.memberships.list | Lists the members. |
POST /v1/loyalty/memberships | loyalty.memberships.create | Enrols a contact or a company in a programme. |
GET /v1/loyalty/memberships/lookup | loyalty.memberships.lookup | Reads the tier, perks, balance and bonus agreement of a contact or company. |
GET /v1/loyalty/memberships/{id} | loyalty.memberships.get | Reads one membership. |
PUT /v1/loyalty/memberships/{id} | loyalty.memberships.update | Pauses, resumes or ends a membership. |
DELETE /v1/loyalty/memberships/{id} | loyalty.memberships.delete | Deletes a membership that has no bookings, bonus agreements or referral code. |
GET /v1/loyalty/memberships/{id}/progress | loyalty.memberships.progress | Reads the way of a member to the next tier. |
POST /v1/loyalty/memberships/{id}/adjust | loyalty.memberships.adjust | Corrects a member's balance by hand. |
POST /v1/loyalty/memberships/{id}/referral-code | loyalty.memberships.referral-code | Reads or creates the referral code of a member. |
POST /v1/loyalty/membershipsbody: requiredprogram_id. Name the holder withcontact_idororganization_id, whichever the programme's holder needs. Optional:market. A membership always startsactive; astatussent on create is ignored, so change it withPUT.GET /v1/loyalty/memberships/lookupquery:contact_idororganization_id.GET /v1/loyalty/membershipsfilters:program_id,holderandholder_key,contact_id,organization_id,tier_id,tier_code,status,market,external_id, and the balancespoints_balance,points_periodandpoints_lifetime.POST .../adjustbody: requiredpointsandreason. Optional:key.POST .../referral-codebody: optionalcontact_id. A company membership needs it.- Errors:
400withholder_requiredwhen the id the programme's holder needs is missing (contact_idororganization_id), and on lookup when neither is sent.409withprogram_inactivewhen the programme is not active.409withmembership_endedwhen an ended membership is set active again.409withmembership_in_usewhen a delete is asked for a membership that has bookings, bonus agreements or a referral code.409within_trashwhen the programme is in the trash. - Referral code errors:
400withcontact_requiredfor a company membership withoutcontact_id.409withmembership_inactive.503withcode_unavailablewhen no free code can be drawn; try again.
Point bookings
| Operation | Scope | What it does |
|---|---|---|
GET /v1/loyalty/point-ledger | loyalty.points.list | Lists the point bookings. |
GET /v1/loyalty/point-ledger/{id} | loyalty.points.get | Reads one point booking. |
POST /v1/loyalty/points/award | loyalty.points.award | Credits points to a member for a reason. |
- Filters:
membership_id,entry_key,kind,points,remaining,order_id,reward_id,redemption_idandreason. POST /v1/loyalty/points/awardbody: requiredpoints. Name the member withmembership_id,contact_idororganization_id. Optional:program_code,reasonandkey.- Award errors:
409withprogram_ambiguouswhen the member is in several active points programmes. Sendprogram_codeto pick one; it is read only when the member is named bycontact_idororganization_id.404withno_membershipwhen the member has no active membership in an active points programme; a programme withauto_enrollenrols the holder first.409withmembership_inactive.400withnot_points_programme.
Referral codes and referrals
| Operation | Scope | What it does |
|---|---|---|
GET /v1/loyalty/referral-codes | loyalty.referrals.codes-list | Lists the referral codes. |
GET /v1/loyalty/referral-codes/{id} | loyalty.referrals.codes-get | Reads one referral code. |
POST /v1/loyalty/referral-codes/{id}/deactivate | loyalty.referrals.codes-deactivate | Retires a referral code. |
GET /v1/loyalty/referrals | loyalty.referrals.list | Lists the referrals. |
GET /v1/loyalty/referrals/{id} | loyalty.referrals.get | Reads one referral. |
POST /v1/loyalty/referrals/claim | loyalty.referrals.claim | Claims a referral code for a new customer. |
POST /v1/loyalty/referrals/{id}/reward | loyalty.referrals.reward | Issues the referrer reward of a qualified referral again. |
POST /v1/loyalty/referrals/{id}/reject | loyalty.referrals.reject | Rejects a referral with a reason. |
- Referral codes filters:
membership_id,contact_id,code,usesandactive. - Referrals filters:
referral_code_id,referrer_membership_id,referrer_contact_id,referee_contact_id,state,order_id,reason,referee_voucher_codeandreferrer_voucher_code. POST /v1/loyalty/referrals/claimbody: requiredcodeandcontact_id.- Claim errors:
400withcode_and_contact_requiredwhencodeorcontact_idis missing.404withunknown_codefor an unknown or retired code.409withself_referralwhen the new customer uses their own code.409withalready_referredwhen the customer was already referred with another code. POST .../rejectbody: requiredreason.POST .../rewardandPOST .../deactivate: take no body.rewardanswers409withnot_qualified,no_reward_promotionorno_referrer, and502withvoucher_unavailable.rejectanswers409withreferral_closed.
Turnover bonus agreements
| Operation | Scope | What it does |
|---|---|---|
GET /v1/loyalty/bonus-agreements | loyalty.bonus.list | Lists the bonus agreements. |
POST /v1/loyalty/bonus-agreements | loyalty.bonus.create | Opens the agreement of a company for a year. |
GET /v1/loyalty/bonus-agreements/{id} | loyalty.bonus.get | Reads one agreement. |
PUT /v1/loyalty/bonus-agreements/{id} | loyalty.bonus.update | Corrects the base, the document or the turnover source of an agreement. |
GET /v1/loyalty/bonus-agreements/{id}/progress | loyalty.bonus.progress | Reads where a company stands in its bonus year. |
POST /v1/loyalty/bonus-agreements/{id}/rebuild | loyalty.bonus.rebuild | Reads the completed orders of the company for the year into the agreement again. |
POST /v1/loyalty/bonus-agreements/{id}/calculate | loyalty.bonus.calculate | Calculates the bonus after the year has ended. |
POST /v1/loyalty/bonus-agreements/{id}/approve | loyalty.bonus.approve | Approves a calculated bonus. |
POST /v1/loyalty/bonus-agreements/{id}/pay | loyalty.bonus.pay | Records that an approved bonus was paid. |
POST /v1/loyalty/bonus-agreements/{id}/forfeit | loyalty.bonus.forfeit | Forfeits a calculated or approved bonus with a reason. |
GET /v1/loyalty/turnover-entries | loyalty.bonus.entries-list | Lists the turnover lines of the agreements. |
POST /v1/loyalty/turnover-entries | loyalty.bonus.entries-correct | Corrects the turnover of an agreement by hand. |
GET /v1/loyalty/turnover-entries/{id} | loyalty.bonus.entries-get | Reads one turnover line. |
POST /v1/loyalty/turnover-imports | loyalty.bonus.import | Imports turnover figures from the ERP. |
POST /v1/loyalty/bonus-agreementsbody: requiredprogram_idandorganization_id. Optional:period,base_turnover(kept as entered by hand),document_refandcontact_id. Without a base, the previous agreement's final turnover is used, else the company's completed orders of last year. If those orders cannot be read, the base is 0 andbase_derivationsaysorders_unavailable.- Agreement filters:
program_id,membership_id,organization_id,period,period_start,period_end,base_source,turnover_source,reached_tier_id,reached_tier_codeandprojected_tier_code. POST .../calculateandPOST .../rebuildbodies: take no body.POST .../approvebody: optionalchecked_byandreceivables_cleared. While the tenant settingbonus_requires_no_overdueis on (the default),receivables_clearedmust betrue.POST .../paybody: optionalpayout_ref.POST .../forfeitbody: requiredreason.POST /v1/loyalty/turnover-entriesbody: requiredagreement_id,amountandref. Optional:keyandat. Filters:agreement_id,entry_key,source,refandorder_id.POST /v1/loyalty/turnover-importsbody: requiredrows, an array of turnover lines, up to 500 (400too_many_rows;400rows_requiredwhen it is missing). Lines that cannot be booked come back inrejectedwith a reason:no_agreement,agreement_ambiguous,agreement_closedorref_and_amount_required.- Agreement errors:
400withnot_turnover_programmewhen the programme is not a turnover bonus programme.400withinvalid_basewhenbase_turnoveris below zero.400withreason_requiredwhen a forfeit has no reason.409withagreement_existswhen the company already has an agreement for that year.409withperiod_not_overwhen calculate runs before the year has ended.409withnot_calculatedwhen approve runs before the bonus is calculated.409withreceivables_unconfirmedwhen approve runs withoutreceivables_cleared: truewhile the check is on.409withnot_approvedwhen pay runs before approve.409withagreement_closedwhen an approved, paid or forfeited agreement is calculated, rebuilt or corrected, or when its base is changed after it is no longer active.502withorders_unavailablewhen rebuild cannot read the company's orders.
Recalculate
| Operation | Scope | What it does |
|---|---|---|
POST /v1/loyalty/recalculate/run | loyalty.jobs.recalculate | Runs the hourly loyalty job by hand. The same job also runs on the hourly schedule. |
Vocabularies
| Operation | Scope | What it does |
|---|---|---|
GET /v1/loyalty/vocabularies | loyalty.vocabularies.list | Lists the closed vocabularies the app enforces. |
GET /v1/loyalty/vocabularies/{name} | loyalty.vocabularies.get | Reads one vocabulary with its values and labels. An unknown name is 404. |