Skip to content

Promotions Engine

The Promotions Engine delivers time-windowed marketing campaigns. Operators schedule campaigns; the engine computes each campaign's live status from its start/end dates and handles claiming, which grants a bonus through the Bonus Engine.

Public surface

MemberReturnsDescription
getPromotions(options?)Promise<Promotion[]>Campaigns; ACTIVE by default, scheduled/expired on demand
claim(promotionId, payload?)Promise<PlayerBonus>Claim a campaign; grants its linked bonus

Typed structures

typescript
type PromotionStatus = 'SCHEDULED' | 'ACTIVE' | 'EXPIRED'; // computed from dates; never stored
// What claiming grants; the same union as bonus templates:
type PromotionRewardType = BonusTemplate['type']; // 'DEPOSIT_MATCH' | 'FREE_SPINS' | 'CASHBACK' | 'NO_DEPOSIT'

interface Promotion {
  id: string;
  name: string;
  description: string;
  rewardType: PromotionRewardType;
  status: PromotionStatus;
  templateId: string;         // the Bonus Engine template granted on claim
  startDate: string;          // ISO-8601
  endDate: string;            // ISO-8601
  imageUrl?: string;
  claimed: boolean;           // has THIS player already claimed it
  minDeposit?: number;        // deposit eligibility for deposit-linked rewards
}

interface GetPromotionsOptions {
  includeScheduled?: boolean; // default false
  includeExpired?: boolean;   // default false
}

interface ClaimPayload {
  depositTransactionId?: string; // explicit deposit linkage (see below)
}

The reward's grant shape comes entirely from the linked bonus template (typed per variant; see the Bonus Engine); the promotion only adds the time window and eligibility values.

Computed status

text
GIVEN a campaign with startDate S and endDate E
WHEN the current time is before S / between S and E / after E
THEN its status is SCHEDULED / ACTIVE / EXPIRED respectively.

GIVEN a getPromotions() call without options
WHEN it resolves
THEN only ACTIVE campaigns are returned; scheduled and expired are opt-in
     via includeScheduled / includeExpired (teaser and history UIs),
     with deterministic ordering: startDate descending, then id.

Claiming a campaign

Claiming is a cross-engine transaction: the Promotions Engine validates the campaign and hands off to the Bonus Engine, which grants the bonus from the linked template.

javascript
const bonus = await sdk.engagement.promotions.claim('promo_welcome_100', {
  depositTransactionId: 'tx_123' // optional explicit deposit linkage
});
bonus.name;                // '100% Welcome Bonus'
bonus.wageringRequirement; // from the template's wageringMultiplier
bonus.status;              // 'ACTIVE'
text
GIVEN an ACTIVE, unclaimed campaign whose eligibility is met
WHEN claim(promotionId, payload?) is called
THEN a bonus is granted per the linked template's variant rules,
     the campaign's claimed flag flips for this player,
     and the bonus appears in getActiveBonuses().

GIVEN a DEPOSIT_MATCH campaign with a campaign-level minDeposit and a linked
     template with its own minDeposit
WHEN the effective minimum Dmin is needed
THEN Dmin = the GREATER of the campaign-level and template-level values.

GIVEN a DEPOSIT_MATCH campaign with effective minimum Dmin
WHEN claim is called with an explicit depositTransactionId
THEN that deposit is the linkage: if its amount ≥ Dmin and it completed
     within the campaign window, the bonus grants
     matchRate × depositAmount (capped at maxBonusAmount) in the deposit's
     currency; otherwise the claim rejects with
     ENG_PROMOTION_NO_QUALIFYING_DEPOSIT.

GIVEN a DEPOSIT_MATCH campaign with effective minimum Dmin
WHEN claim is called WITHOUT an explicit deposit
THEN the player's LATEST qualifying deposit within the campaign window
     (amount ≥ Dmin) is the linkage; if none exists, the claim rejects
     with ENG_PROMOTION_NO_QUALIFYING_DEPOSIT.

GIVEN an already-claimed campaign
WHEN claim is called again
THEN it rejects with ENG_PROMOTION_ALREADY_CLAIMED (one grant per player per campaign).

Tournament-entry giveaways are not a promotion type: joining a tournament is optIn; the engines stay separate on purpose.

Error contract

CodeError classHTTP statusTrigger conditionRetryable
ENG_PROMOTION_NOT_FOUNDPromotionNotFoundError404Unknown promotionIdNo
ENG_PROMOTION_NOT_ACTIVEPromotionNotActiveError409Claim on a non-ACTIVE campaignNo (until it starts)
ENG_PROMOTION_ALREADY_CLAIMEDPromotionAlreadyClaimedError409Second claim by the same playerNo
ENG_PROMOTION_NO_QUALIFYING_DEPOSITNoQualifyingDepositError400No qualifying deposit in the campaign windowNo (deposit first, then claim)
ENG_PROMOTION_NOT_CLAIMABLEPromotionNotClaimableError409claim on a CASHBACK campaign; the bonus grants automatically at each period close (see Bonus Engine), so CASHBACK campaigns are display-onlyNo

Common errors are defined once in the Error Reference.

Operator-configurable values

ValueTypeDefault / rangeEffect
Campaign catalog + schedulesper campaignoperator-definedWhich campaigns run, and when
rewardType + linked templateper campaignoperator-definedWhat claiming grants
minDeposit eligibilitynumberoperator-definedDeposit-linked campaign gating

Which campaigns a player sees is decided by the backend per player; targeting is not part of the SDK surface. Status computation and the claim/grant rules are platform-fixed.

Sandbox

Seeds cover every scheduling state, including a campaign activating in seconds, so the SCHEDULED→ACTIVE transition is demonstrable without waiting: an ACTIVE deposit-match, an ACTIVE already-claimed campaign, an ACTIVE cashback campaign, a SCHEDULED teaser, and an EXPIRED entry for history UIs.