Free Spins & Free Chips
Give your players free rounds in Slotty Labs games: free spins on the slot games (Slotty Slots and Popworks) and free chips on the other games. Each free round is played at a stake you choose; it costs the player nothing and nothing is debited from their balance. Only the winnings are paid, as cash or as bonus money, as you decide per offer.
You create offers in your dashboard and grant them to players in the dashboard or through the API.
Direct integrations
This page is for casinos integrated directly with Slotty Labs. If you reach our games through an aggregator, use your aggregator's free-round tools.
How it works
- Slotty Labs enables free spins and/or free chips for your casino. Until then, Free spins & chips in your dashboard says they are not enabled for your casino; contact Slotty Labs to enable them.
- Switch the feature on in the games you want (Games → game → Config → Bonus features, or the Bonus features matrix: see Operator Dashboard).
- Create an offer under Free spins & chips → Create, save it as a draft, then activate it (with a reason).
- Grant it to existing players: in the dashboard, or with
POST /api/v1/wallet/offers/grants. - The player opens one of the offer's games. The game shows the offer and plays the free rounds at the offer's stake.
- Follow it with the
offer.*webhooks and the figures on the offer's page.
Welcome spins
Subscribe to the player.created webhook and grant your welcome offer when it arrives. It fires at the player's first launch, so the player exists and the grant is accepted.
Creating an offer
| Setting | Meaning |
|---|---|
| Kind | Free spins (slot games) or free chips (the other games) |
| Name | For your team only |
| Title | What players see in the game: up to 40 characters, without % < > & " |
| Reference | Optional: your own reference for the offer, used as campaignRef in the API |
| Games | Up to 16 games of that kind; a game is usable when the feature is switched on there |
| Stake per currency | The stake of each free round. A currency left empty is not offered. Stakes are checked against each game's bet limits |
| Free rounds per grant | How many free spins (or chips) one grant gives |
| Validity | Hours a player has to use a grant |
| Dates | When grants can be issued (no end date = open-ended) |
| Budget | The most grants this offer can issue to real players (empty = no limit) |
| Per-player limit | How many grants of this offer one player can receive |
| Payout | Cash (default) or Bonus: see below |
An offer goes Draft → Active ⇄ Paused → Archived. Grants can be issued only while it is active and inside its dates. Pausing or archiving an offer stops new grants; grants already issued keep running until they are used, expire or are cancelled (the offer's page has Cancel all grants).
If Slotty Labs pauses one of your offers, the offer's history shows it with the reason, and only Slotty Labs can resume it: contact us. You can still archive it.
Frozen once active. After activation only the name, the end date, the budget (never below the grants already issued) and the per-player limit can change. To change anything else, duplicate the offer.
Payout: cash or bonus
- Cash. Winnings are paid to the player's real (withdrawable) balance at once, even while the player has an open deposit bonus. A free round can win up to the game's own maximum win.
- Bonus. Winnings go to the bonus balance and must be wagered (the wagering multiplier × the winnings) before they become cash. You set the wagering multiplier, how long the bonus money stays wagerable, the largest bet allowed while it is wagered, and optionally the most one grant can turn into cash (per currency).
UK licence: 10× wagering cap
If your casino holds a UK (UKGC) licence, bonus-paid offers are limited to 10× wagering. Cash payout is not affected. A grant call for a bonus offer above the cap for your current licences is refused with wagering_cap.
Cash winnings from offers, and bonus money that converts to cash, count as payouts in your GGR, the same way as your bonuses. The free rounds themselves move no money.
Granting
In the dashboard
On the offer's page, Grant takes up to 1,000 player ids (or a CSV), the currency, Real players or Test (play money), and a reason, and shows the result per player. Test in Demo link grants a test offer to your Demo link player, so you can try the offer in the game in play money.
Through the API
POST https://api.slottylabs.com/api/v1/wallet/offers/grants
Authorization: Bearer sk_production_...
Content-Type: application/json
{
"campaignRef": "welcome-fs",
"requestId": "crm-20261105-0001",
"players": [{ "playerId": "ext-123", "currency": "USDC", "externalRef": "crm-77" }],
"expiresAt": "2026-11-12T00:00:00Z"
}| Field | Rules |
|---|---|
campaignId or campaignRef | The offer: its id (UUID) or your reference |
requestId | Your idempotency key: 8–64 characters (letters, digits, _ . : -) |
players | 1–1,000 players, each { playerId, currency, externalRef? }: your player id (1–128 characters), one of the offer's currencies, and optionally your own reference for the grant (1–64 characters, unique per offer) |
expiresAt | Optional ISO 8601 time. A grant expires at the earlier of this and the offer's validity, and never less than 1 hour after the grant |
The answer is 201 (or 200 for a replay):
{
"success": true,
"data": {
"requestId": "crm-20261105-0001",
"environment": "production",
"granted": 1,
"refused": 0,
"results": [
{ "playerId": "ext-123", "status": "granted", "grantId": "0f6b7a52-3c2e-4d7f-9a51-1b2c3d4e5f60", "reason": null }
]
}
}Idempotency. Sending the same requestId with the same body again returns the first result: granted players come back replayed and nothing is granted twice (a player the first call refused comes back refused with reason not_granted). The same requestId with a different body is refused with 409 idempotency_conflict. A 503 with retryable: true (reason busy) means nothing was written: send the same request again.
A refused player does not fail the call. Their result has status: "refused" and a reason:
reason | Meaning |
|---|---|
player_not_found | No player with this id in your casino yet (a player exists from their first launch) |
player_blocked | The player is suspended or excluded |
per_player_limit | The player already holds the most grants of this offer |
currency_not_offered | The offer has no stake in that currency |
budget_exhausted | The offer's budget is used up (real-player grants) |
duplicate_player | The player is listed twice in this request |
external_ref_taken | This externalRef is already used by another grant of this offer |
not_granted | A replay: the original call refused this player |
A refused call grants nothing: see Error Codes → Free spins & chips.
Real players and tests
Every grant has an environment:
- Real players (
production): played only in real-money sessions. - Test (
sandbox): played only in play money: launch the player withdemo: true(or use the Demo link), and nothing reaches their real balance. Test grants never count against the budget and are kept apart from real-player figures.
A sandbox API key grants and sees test grants only. Real-player grants through the API are enabled per casino by Slotty Labs: until they are enabled for your casino, every API grant is a test grant and the API lists and cancels test grants only, whatever key you use. Real-player grants can be made in the dashboard. The environment of the answer always says which kind you made.
Reading and cancelling
| Method | Path | Returns |
|---|---|---|
GET | /api/v1/wallet/offers/campaigns | Your active and paused offers: id, reference, kind, games, free rounds, validity, dates, limits, payout and the stake per currency |
GET | /api/v1/wallet/offers/grants | Grants, newest first: { items, next }. Query: playerId, campaignId, status, limit (1–200, default 50), before (the previous page's next) |
GET | /api/v1/wallet/offers/grants/:grantId | One grant |
POST | /api/v1/wallet/offers/grants/:grantId/cancel | Cancels an active grant. Body { "reason": "…" } (3–500 characters); returns the cancelled grant |
GET | /api/v1/wallet/offers/player | A player's open offers, for your lobby (active grants, paused ones included, soonest expiry first). Query: playerId, currency (optional) |
A key reads and cancels only the grants of its own environment. Cancelling a grant that is no longer active is refused (409 grant_not_active); a round already under way still finishes.
The grant object
The grant calls, the reads and the offer.* webhooks all carry this object:
{
"grantId": "0f6b7a52-3c2e-4d7f-9a51-1b2c3d4e5f60",
"campaignId": "7d9e8f00-1111-4222-8333-444455556666",
"campaignRef": "welcome-fs",
"externalRef": "crm-77",
"playerId": "ext-123",
"kind": "freespin",
"title": "10 welcome spins",
"environment": "production",
"currency": "USDC",
"stake": "1.000000",
"stakeMinor": "1000000",
"quantity": 10,
"used": 3,
"remaining": 7,
"games": ["slotty-slots"],
"payout": { "mode": "cash" },
"totalWin": "12.500000",
"totalWinMinor": "12500000",
"status": "active",
"paused": false,
"pausedReason": null,
"startsAt": "2026-11-05T10:00:00.000Z",
"expiresAt": "2026-11-12T00:00:00.000Z",
"endedAt": null,
"endReason": null
}- Money is a decimal string at the currency's precision (
stake,totalWin) plus the same amount in integer minor units (stakeMinor,totalWinMinor). Amounts of different currencies are never added together. payoutis{ "mode": "cash" }or{ "mode": "bonus", "wageringMultiplier", "bonusExpiresHours", "maxBet", "maxBetMinor", "maxCashout", "maxCashoutMinor" }(maxCashoutnull = no limit).status:active,completed(every free round used),expired(the expiry passed with free rounds left),cancelled, orforfeited(the player self-excluded or the account was closed).paused(withpausedReason): the grant cannot be played right now, because Slotty Labs switched the feature off for your casino (not_entitled), none of the offer's games has it switched on (games_switched_off), or the player is suspended or on a timed exclusion (player_blocked). Its expiry keeps running, and it resumes by itself if the cause goes away before then.
A round that is already under way finishes even if its grant expires or is cancelled meanwhile (not if it is forfeited). If a free round is refunded (its round rolled back), the free round is given back to the grant.
Webhooks
| Event | Sent when |
|---|---|
offer.granted | A grant was issued |
offer.completed | The player used every free round of a grant |
offer.expired | A grant reached its expiry with free rounds left |
offer.cancelled | A grant was cancelled (dashboard or API) |
offer.forfeited | A grant ended because the player self-excluded or the account was closed |
The data of each is the grant object plus playerUuid (our internal player id). Test grants send them too: check data.environment. Subscribe under Settings → Webhooks; see Webhooks.
With the SDK
SDK version 1.9.0 or later has client.offers (reference):
import { SlottyClient, SlottyApiError } from '@slottylabs/sdk';
const client = new SlottyClient({ apiKey: process.env.SLOTTY_API_KEY!, webhookSecret: process.env.SLOTTY_WEBHOOK_SECRET! });
try {
const res = await client.offers.grant({
campaignRef: 'welcome-fs',
requestId: 'welcome-ext-123',
players: [{ playerId: 'ext-123', currency: 'USDC' }],
});
for (const r of res.results) {
if (r.status === 'refused') console.warn(`${r.playerId}: ${r.reason}`);
}
} catch (err) {
if (err instanceof SlottyApiError && err.reason === 'not_entitled') {
// Free spins are not enabled for your casino yet
} else {
throw err;
}
}Figures
The offer's page shows, per currency (real players and tests apart): grants, players, free rounds granted and used, the stake value used, winnings, paid as cash, added to bonus, and converted. Return (winnings ÷ free-play stake) is shown to you only, never to players.

