Loyalty

The Loyalty app runs points programmes and turnover bonus agreements for companies, with tiers, rewards, memberships and referral codes.

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

ObjectWhat it holds
Programmecode, labels, kind (points or turnover bonus), holder (contact or organisation), period, currency, auto_enroll and active.
TierThe programme it belongs to, code, labels, position, the threshold (threshold_kind and threshold_value), bonus_percent, points_multiplier and perks.
Rewardcode, points_cost, the promotion_id whose vouchers it issues, min_tier_id and active.
MembershipOne contact or company in one programme: its tier, status, and balances of points (period, lifetime and current).
Point bookingOne entry of the point ledger: kind, points, remaining, the order, reward or redemption it came from, and reason.
Reward redemptionA reward redeemed by a member, with its voucher_code, the points it cost and its state.
Referral codeA member's code, its uses and whether it is active.
ReferralWho referred whom, its state, the order, the reason and the voucher codes issued to referee and referrer.
Bonus agreementOne company and year: the programme, the base turnover, the turnover so far, the projection, the reached tier, the bonus and its settlement state.
Turnover entryOne 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 first DELETE, a programme with bonus agreements also answers has_members.
  • has_agreements: a trashed programme that still has bonus agreements, at the second DELETE (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

  • key is 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 answers 201 and created: true. A repeat of the same key answers 200 and created: false and 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 a key, every call books again.
  • Sync fields are external_id, external_refs, source_data and source_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 on external_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

OperationScopeWhat it does
GET /v1/loyalty/programsloyalty.programs.listLists the programmes of the tenant.
POST /v1/loyalty/programsloyalty.programs.createCreates a programme.
GET /v1/loyalty/programs/{id}loyalty.programs.getReads one programme.
PUT /v1/loyalty/programs/{id}loyalty.programs.updateCorrects a programme.
DELETE /v1/loyalty/programs/{id}loyalty.programs.deleteMoves a programme into the trash, with its tiers and rewards.
POST /v1/loyalty/programs/{id}/restoreloyalty.programs.restoreBrings a programme back out of the trash.
POST /v1/loyalty/programs/trash/emptyloyalty.programs.empty-trashDeletes every programme in the trash for good.
POST /v1/loyalty/programs/trash/purgeloyalty.programs.purgeDeletes the programmes that outlived the trash retention. It is the nightly purge, callable by hand.
GET /v1/loyalty/programs/{id}/figuresloyalty.programs.figuresReads the active members of a programme per tier.
POST /v1/loyalty/programs/{id}/open-periodloyalty.programs.open-periodOpens the yearly bonus agreements for every member of a turnover bonus programme.
  • GET /v1/loyalty/programs filters: kind (for example turnover_bonus), active, holder, currency, period, auto_enroll, member_count, external_id and code, plus trashed.
  • POST /v1/loyalty/programs body: required code, labels and kind, plus holder for a points programme. Optional: period, currency, auto_enroll, active, description, metadata and the sync fields. A turnover bonus programme takes holder: organization and period: calendar_year by default. A programme created with the X-Revenexx-Market header belongs to that market; without it, the programme applies in every market.
  • PUT /v1/loyalty/programs/{id} errors: 400 with kind_fixed when the kind or the holder is changed.
  • POST /v1/loyalty/programs errors: 409 with code_taken when the code is in use, and 409 with in_trash when a trashed programme has that code.
  • open-period body: period, a four-digit year (default the current year). A points programme is refused with 400 not_turnover_programme, and a bad year with 400 invalid_period.

Tiers

OperationScopeWhat it does
GET /v1/loyalty/tiersloyalty.tiers.listLists the tiers.
POST /v1/loyalty/tiersloyalty.tiers.createAdds a tier to a programme.
GET /v1/loyalty/tiers/{id}loyalty.tiers.getReads one tier.
PUT /v1/loyalty/tiers/{id}loyalty.tiers.updateCorrects a tier.
DELETE /v1/loyalty/tiers/{id}loyalty.tiers.deleteMoves a tier into the trash.
POST /v1/loyalty/tiers/{id}/restoreloyalty.tiers.restoreBrings a tier back out of the trash.
POST /v1/loyalty/tiers/trash/emptyloyalty.tiers.empty-trashDeletes every tier in the trash for good.
POST /v1/loyalty/tiers/trash/purgeloyalty.tiers.purgeDeletes the tiers that outlived the trash retention.
  • POST /v1/loyalty/tiers body: required program_id, code and labels. Optional: position, threshold_kind, threshold_value, bonus_percent, points_multiplier, perks, active, metadata and the sync fields.
  • Errors: 409 with in_trash when the programme is in the trash. 400 for a value out of range (bonus_percent from 0 to 100, threshold_value 0 or more, points_multiplier above 0), or a threshold_kind of relative_to_base on a points programme. 409 with code_taken for a code in use.

Rewards

OperationScopeWhat it does
GET /v1/loyalty/rewardsloyalty.rewards.listLists the rewards.
POST /v1/loyalty/rewardsloyalty.rewards.createAdds a reward to a points programme.
GET /v1/loyalty/rewards/{id}loyalty.rewards.getReads one reward.
PUT /v1/loyalty/rewards/{id}loyalty.rewards.updateCorrects a reward.
DELETE /v1/loyalty/rewards/{id}loyalty.rewards.deleteMoves a reward into the trash.
POST /v1/loyalty/rewards/{id}/restoreloyalty.rewards.restoreBrings a reward back out of the trash.
POST /v1/loyalty/rewards/trash/emptyloyalty.rewards.empty-trashDeletes every reward in the trash for good.
POST /v1/loyalty/rewards/trash/purgeloyalty.rewards.purgeDeletes the rewards that outlived the trash retention.
POST /v1/loyalty/rewards/{id}/redeemloyalty.rewards.redeemRedeems points for a single-use reward voucher.
  • POST /v1/loyalty/rewards body: required program_id, code, labels, points_cost and promotion_id. Optional: description, min_tier_id, voucher_batch_id, active, metadata and the sync fields.
  • POST .../redeem body: the member is named by membership_id, or by contact_id or organization_id. Optional: key.
  • Redeem errors: 409 with insufficient_points, tier_too_low (the member's tier is below the reward's min_tier_id), reward_inactive, membership_inactive or program_inactive. 400 with wrong_programme, or contact_required when a company membership redeems without contact_id, the person the voucher is issued to. 404 with no_membership when no membership of the reward's programme is found. 502 with voucher_unavailable when the promotions app does not issue a code.

Reward redemptions

OperationScopeWhat it does
GET /v1/loyalty/reward-redemptionsloyalty.rewards.redemptions-listLists the redeemed rewards.
GET /v1/loyalty/reward-redemptions/{id}loyalty.rewards.redemptions-getReads one redeemed reward.
  • Filters: membership_id, reward_id, voucher_code, state, request_key and used_order_id, plus points.

Memberships

OperationScopeWhat it does
GET /v1/loyalty/membershipsloyalty.memberships.listLists the members.
POST /v1/loyalty/membershipsloyalty.memberships.createEnrols a contact or a company in a programme.
GET /v1/loyalty/memberships/lookuployalty.memberships.lookupReads the tier, perks, balance and bonus agreement of a contact or company.
GET /v1/loyalty/memberships/{id}loyalty.memberships.getReads one membership.
PUT /v1/loyalty/memberships/{id}loyalty.memberships.updatePauses, resumes or ends a membership.
DELETE /v1/loyalty/memberships/{id}loyalty.memberships.deleteDeletes a membership that has no bookings, bonus agreements or referral code.
GET /v1/loyalty/memberships/{id}/progressloyalty.memberships.progressReads the way of a member to the next tier.
POST /v1/loyalty/memberships/{id}/adjustloyalty.memberships.adjustCorrects a member's balance by hand.
POST /v1/loyalty/memberships/{id}/referral-codeloyalty.memberships.referral-codeReads or creates the referral code of a member.
  • POST /v1/loyalty/memberships body: required program_id. Name the holder with contact_id or organization_id, whichever the programme's holder needs. Optional: market. A membership always starts active; a status sent on create is ignored, so change it with PUT.
  • GET /v1/loyalty/memberships/lookup query: contact_id or organization_id.
  • GET /v1/loyalty/memberships filters: program_id, holder and holder_key, contact_id, organization_id, tier_id, tier_code, status, market, external_id, and the balances points_balance, points_period and points_lifetime.
  • POST .../adjust body: required points and reason. Optional: key.
  • POST .../referral-code body: optional contact_id. A company membership needs it.
  • Errors: 400 with holder_required when the id the programme's holder needs is missing (contact_id or organization_id), and on lookup when neither is sent. 409 with program_inactive when the programme is not active. 409 with membership_ended when an ended membership is set active again. 409 with membership_in_use when a delete is asked for a membership that has bookings, bonus agreements or a referral code. 409 with in_trash when the programme is in the trash.
  • Referral code errors: 400 with contact_required for a company membership without contact_id. 409 with membership_inactive. 503 with code_unavailable when no free code can be drawn; try again.

Point bookings

OperationScopeWhat it does
GET /v1/loyalty/point-ledgerloyalty.points.listLists the point bookings.
GET /v1/loyalty/point-ledger/{id}loyalty.points.getReads one point booking.
POST /v1/loyalty/points/awardloyalty.points.awardCredits points to a member for a reason.
  • Filters: membership_id, entry_key, kind, points, remaining, order_id, reward_id, redemption_id and reason.
  • POST /v1/loyalty/points/award body: required points. Name the member with membership_id, contact_id or organization_id. Optional: program_code, reason and key.
  • Award errors: 409 with program_ambiguous when the member is in several active points programmes. Send program_code to pick one; it is read only when the member is named by contact_id or organization_id. 404 with no_membership when the member has no active membership in an active points programme; a programme with auto_enroll enrols the holder first. 409 with membership_inactive. 400 with not_points_programme.

Referral codes and referrals

OperationScopeWhat it does
GET /v1/loyalty/referral-codesloyalty.referrals.codes-listLists the referral codes.
GET /v1/loyalty/referral-codes/{id}loyalty.referrals.codes-getReads one referral code.
POST /v1/loyalty/referral-codes/{id}/deactivateloyalty.referrals.codes-deactivateRetires a referral code.
GET /v1/loyalty/referralsloyalty.referrals.listLists the referrals.
GET /v1/loyalty/referrals/{id}loyalty.referrals.getReads one referral.
POST /v1/loyalty/referrals/claimloyalty.referrals.claimClaims a referral code for a new customer.
POST /v1/loyalty/referrals/{id}/rewardloyalty.referrals.rewardIssues the referrer reward of a qualified referral again.
POST /v1/loyalty/referrals/{id}/rejectloyalty.referrals.rejectRejects a referral with a reason.
  • Referral codes filters: membership_id, contact_id, code, uses and active.
  • Referrals filters: referral_code_id, referrer_membership_id, referrer_contact_id, referee_contact_id, state, order_id, reason, referee_voucher_code and referrer_voucher_code.
  • POST /v1/loyalty/referrals/claim body: required code and contact_id.
  • Claim errors: 400 with code_and_contact_required when code or contact_id is missing. 404 with unknown_code for an unknown or retired code. 409 with self_referral when the new customer uses their own code. 409 with already_referred when the customer was already referred with another code.
  • POST .../reject body: required reason. POST .../reward and POST .../deactivate: take no body. reward answers 409 with not_qualified, no_reward_promotion or no_referrer, and 502 with voucher_unavailable. reject answers 409 with referral_closed.

Turnover bonus agreements

OperationScopeWhat it does
GET /v1/loyalty/bonus-agreementsloyalty.bonus.listLists the bonus agreements.
POST /v1/loyalty/bonus-agreementsloyalty.bonus.createOpens the agreement of a company for a year.
GET /v1/loyalty/bonus-agreements/{id}loyalty.bonus.getReads one agreement.
PUT /v1/loyalty/bonus-agreements/{id}loyalty.bonus.updateCorrects the base, the document or the turnover source of an agreement.
GET /v1/loyalty/bonus-agreements/{id}/progressloyalty.bonus.progressReads where a company stands in its bonus year.
POST /v1/loyalty/bonus-agreements/{id}/rebuildloyalty.bonus.rebuildReads the completed orders of the company for the year into the agreement again.
POST /v1/loyalty/bonus-agreements/{id}/calculateloyalty.bonus.calculateCalculates the bonus after the year has ended.
POST /v1/loyalty/bonus-agreements/{id}/approveloyalty.bonus.approveApproves a calculated bonus.
POST /v1/loyalty/bonus-agreements/{id}/payloyalty.bonus.payRecords that an approved bonus was paid.
POST /v1/loyalty/bonus-agreements/{id}/forfeitloyalty.bonus.forfeitForfeits a calculated or approved bonus with a reason.
GET /v1/loyalty/turnover-entriesloyalty.bonus.entries-listLists the turnover lines of the agreements.
POST /v1/loyalty/turnover-entriesloyalty.bonus.entries-correctCorrects the turnover of an agreement by hand.
GET /v1/loyalty/turnover-entries/{id}loyalty.bonus.entries-getReads one turnover line.
POST /v1/loyalty/turnover-importsloyalty.bonus.importImports turnover figures from the ERP.
  • POST /v1/loyalty/bonus-agreements body: required program_id and organization_id. Optional: period, base_turnover (kept as entered by hand), document_ref and contact_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 and base_derivation says orders_unavailable.
  • Agreement filters: program_id, membership_id, organization_id, period, period_start, period_end, base_source, turnover_source, reached_tier_id, reached_tier_code and projected_tier_code.
  • POST .../calculate and POST .../rebuild bodies: take no body.
  • POST .../approve body: optional checked_by and receivables_cleared. While the tenant setting bonus_requires_no_overdue is on (the default), receivables_cleared must be true.
  • POST .../pay body: optional payout_ref. POST .../forfeit body: required reason.
  • POST /v1/loyalty/turnover-entries body: required agreement_id, amount and ref. Optional: key and at. Filters: agreement_id, entry_key, source, ref and order_id.
  • POST /v1/loyalty/turnover-imports body: required rows, an array of turnover lines, up to 500 (400 too_many_rows; 400 rows_required when it is missing). Lines that cannot be booked come back in rejected with a reason: no_agreement, agreement_ambiguous, agreement_closed or ref_and_amount_required.
  • Agreement errors: 400 with not_turnover_programme when the programme is not a turnover bonus programme. 400 with invalid_base when base_turnover is below zero. 400 with reason_required when a forfeit has no reason. 409 with agreement_exists when the company already has an agreement for that year. 409 with period_not_over when calculate runs before the year has ended. 409 with not_calculated when approve runs before the bonus is calculated. 409 with receivables_unconfirmed when approve runs without receivables_cleared: true while the check is on. 409 with not_approved when pay runs before approve. 409 with agreement_closed when an approved, paid or forfeited agreement is calculated, rebuilt or corrected, or when its base is changed after it is no longer active. 502 with orders_unavailable when rebuild cannot read the company's orders.

Recalculate

OperationScopeWhat it does
POST /v1/loyalty/recalculate/runloyalty.jobs.recalculateRuns the hourly loyalty job by hand. The same job also runs on the hourly schedule.

Vocabularies

OperationScopeWhat it does
GET /v1/loyalty/vocabulariesloyalty.vocabularies.listLists the closed vocabularies the app enforces.
GET /v1/loyalty/vocabularies/{name}loyalty.vocabularies.getReads one vocabulary with its values and labels. An unknown name is 404.
Was this page helpful?