Appearance
Freeplays
Freeplays (free spins or tickets) are bets that an operator grants to a player. A player can spend a freeplay without a wallet debit. Each freeplay has a fixed stake, and optionally a fixed feature, set when the operator grants the package.
How It Works
Operators grant and revoke freeplay packages out-of-band, through the Operator API. The engine sends the currently available packages to your frontend as part of the normal session calls. There is no separate "claim" endpoint. Read freePlaysAvailable and spend a ticket with placeBet.
Reading Available Freeplays
freePlaysAvailable (an array of IFreePlayInfo) is included in:
login()/refresh():IConnectReply.freePlaysAvailableloadConfig():ILoadConfigReply.freePlaysAvailable
typescript
const response = await login({ loginURL, launchToken });
if (response.success) {
const freePlays = response.result.freePlaysAvailable ?? [];
}Not echoed by placeBet
IPlaceBetReply does not carry freePlaysAvailable. See Keeping the List in Sync to update your local copy after you spend a ticket.
Each entry groups tickets that share a stake, a currency, and (optionally) a feature. A player can hold several entries at the same time:
typescript
interface IFreePlayInfo {
currency: string; // ISO 4217 currency code
stake: number; // Fixed stake amount for each free play
count: number; // Number of free plays available
feature?: string; // Optional feature to enter directly (e.g. "freespin")
}Helpers
Two helper functions summarize the array. You do not need to reduce the array yourself:
typescript
import { getFreePlaysRemaining, getFreePlayStake } from '@hizi.io/engine-sdk';
const totalRemaining = getFreePlaysRemaining(freePlays); // sum of `count` across all entries
const firstStake = getFreePlayStake(freePlays); // first entry's `stake`, or undefined if noneTIP
getFreePlayStake only reads the first entry. If a player can hold packages at more than one stake or feature, do not rely on this helper alone. Iterate freePlaysAvailable yourself and build a picker.
Choosing a Freeplay
Sometimes more than one package is available, with different stakes, or a stake tied to a specific feature such as "freespin". Present the options so the player can pick which one to spend:
typescript
const options = freePlays.map((fp, i) => ({
value: i,
label: fp.feature ? `${fp.stake} · ${fp.feature} (${fp.count} left)` : `${fp.stake} (${fp.count} left)`,
}));Spending a Freeplay
Call placeBet with useTicket: true, the ticket's own stake, and (if set) its feature as useTicketFeatureType:
typescript
const freePlay = freePlays[selectedIndex]; // e.g. { currency: 'EUR', stake: 100, count: 5, feature: 'freespin' }
const response = await placeBet({
backendURL,
token: sessionToken,
stake: freePlay.stake,
useTicket: true,
useTicketFeatureType: freePlay.feature,
config,
});
if (response.success) {
const gameResult = response.result.result;
// Handle exactly like any other placeBet result - multi-step rounds,
// player choices, and wager/collect flows all still apply.
}Still a normal round
A freeplay spin returns a normal IGameResult. It runs through the full game flow (multi-step rounds, player choices, wager/collect). The only difference is that the engine does not debit the stake from the player's balance.
Keeping the List in Sync
The reply from placeBet does not echo freePlaysAvailable. Spending a ticket does not update your copy of the array. Use one of two ways to keep it correct:
Optimistic decrement. In your own client-side state, decrement the
countof the entry you just spent. Drop the entry once its count reaches0:typescriptsetFreePlays(prev => prev .map(fp => (fp === freePlay ? { ...fp, count: fp.count - 1 } : fp)) .filter(fp => fp.count > 0), );Resync from the engine. Call
loadConfig()(orrefresh()) again to pull the authoritative list from the RGS. Do this at minimum on every session refresh. Operators can grant or revoke packages while a game is open.
TIP
Prefer resyncing over trusting the optimistic decrement long-term. Treat the optimistic decrement as a fast local update between the moment the player spends a ticket and the next loadConfig()/refresh() call. Do not treat it as a permanent source of truth.