Skip to content

Free Rounds (Free Spins)

Free rounds (also called free spins) let a player spin a game without staking their own cash — the stake is funded by a grant you issue, and any winnings are credited to the player.

There are two sides to free rounds:

  • Issuing — you call POST /v1/free-rounds to grant a player free rounds on one or more games (see Issuing free rounds below).
  • Receiving — the resulting wallet events arrive at your existing callback endpoint as ordinary bet / win callbacks carrying one extra signal, is_free: true. There is no new endpoint to implement on your side to receive them.

If you have not yet integrated the standard wallet callback, read Wallet callbacks first. This guide describes what is different for free rounds.


Issue a grant with POST /v1/free-rounds (API reference). count is per gamegames: [A, B] with count: 10 issues 10 rounds on each (20 total). The grant is not tied to a session; each game must support free rounds and passes the same jurisdiction check as a launch.

Check free_rounds_support on the game (from GET /v1/games or GET /v1/games/{id}) before issuing — a game where it is false is rejected at grant time.

Terminal window
curl -X POST https://api.aggregator.gg/v1/free-rounds \
-H "Authorization: Bearer sk_live_xxx" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"provider_code": "truelabs",
"player_id": "player-123",
"games": ["550e8400-e29b-41d4-a716-446655440000"],
"count": 10,
"currency": "EUR",
"country": "DE",
"coin_level": 1,
"start_at": "2026-07-01T00:00:00Z",
"end_at": "2026-07-10T00:00:00Z"
}'
  • Scope + idempotency. Requires the write scope and an Idempotency-Key header — the same key + same body replays the cached result; same key + different body → 409.
  • Stake. Provide exactly one of coin_level or bet_amount. Which one — and the valid values — depend on the game, so call GET /v1/games/{game_id}/stakes?currency= first and branch on the mode it returns:
    • enumerated → we hold the valid list; pick one of the listed stakes and send the field named by stake_field (coin_level for TrueLabs, bet_amount for Apparat).
    • provider_validated → we don’t have the list for this currency, so stakes is empty; send any bet_amount and the provider validates it on its side at issue time (we mirror its accept/reject).
    • unsupported → the game has no free-round stake model.
  • Response. You get back a grant_id and a per-game status. Manage the grant later with GET /v1/free-rounds (list) and POST /v1/free-rounds/{grant_id}/cancel (cancel unplayed rounds).
  • Availability. Free rounds must be enabled for the provider on your account first; until then issuance returns 403.

The rest of this guide covers the receiving side — how the spins reach your wallet endpoint.


A free-round callback is the standard forward payload (see Wallet callbacks → Forward Payload) plus these fields:

Field Type Description
is_free boolean true when this callback belongs to a free round. This is the field you branch on. Absent or false on ordinary cash play.
free_round_grant_id string The grant_id of the free-round grant this spin belongs to — use it to group a campaign’s activity. May be empty if the spin is not tied to a specific grant.
free_round_kind string | null The kind of free round, as you set it when issuing (freespin / freeticket / voucher).

action, transaction_type, transaction_id, amount, currency, player_id, provider_code, and session_id carry exactly the same meaning and units as on a normal callback — amount and balance are integers in minor units (details). Branch on transaction_type and is_free, never on action.


Callback is_free What to do
bet true Do not touch the real-money balance. The grant funds the stake. Record the spin for reporting if you wish; amount is the nominal stake, not a charge.
win true Credit the player by amount, exactly like a normal win. Optionally book it as bonus funds subject to your wagering / withdrawability rules (see below).
bet / win absent / false Ordinary cash play — debit / credit as documented in Wallet callbacks.

The net effect of a correctly handled free round is that the player risks nothing (free bet is not debited) and keeps the winnings (free win is credited).

// pseudo-code — the only change vs cash play is the is_free guard on bet
if (callback.transaction_type === "bet" && callback.is_free) {
// grant-funded — DO NOT debit. Optionally log for reporting.
balance = currentBalance(callback.player_id);
} else if (callback.transaction_type === "bet") {
balance = debit(callback.player_id, callback.amount); // normal cash bet
} else if (callback.transaction_type === "win") {
balance = credit(callback.player_id, callback.amount); // free or cash — credit either way
}

is_free exists so you can apply your own promotional accounting. Free-round winnings are commonly treated as bonus funds subject to a wagering requirement before withdrawal. Whether you credit them as cash or as a restricted bonus balance is entirely your decision — The Aggregator does not impose a model. If you do nothing special, crediting them as cash is valid and the player simply keeps the winnings.


Provider differences — don’t assume a bet always precedes a win

Section titled “Provider differences — don’t assume a bet always precedes a win”

The callback sequence varies by game. Your handler must cope with both shapes, which is why you branch on is_free per callback rather than assuming a bet always arrives before a win:

  • Both legs. Some free rounds produce a free bet (is_free: true, nominal stake) followed, on a winning spin, by a free win (is_free: true).
  • Win only. Others settle as a single free win (is_free: true) carrying the total winnings, with finished: trueno free bet is sent. A zero-win round still settles (you may receive a win with amount: 0).

Treat each callback independently by transaction_id and is_free. Do not require a matching free bet before accepting a free win.


A free spin’s bet leg (per-spin provider) — record, do not debit:

{
"action": "BET",
"transaction_type": "bet",
"is_free": true,
"free_round_grant_id": "9f1c0b3a-2d4e-4a6b-8c0d-1e2f3a4b5c6d",
"free_round_kind": "freespin",
"transaction_id": "provider-unique-tx-id",
"amount": 20,
"currency": "EUR",
"player_id": "player-123",
"provider_code": "apparat",
"session_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}

The free win — credit the player:

{
"action": "WIN",
"transaction_type": "win",
"is_free": true,
"free_round_grant_id": "9f1c0b3a-2d4e-4a6b-8c0d-1e2f3a4b5c6d",
"free_round_kind": "freespin",
"transaction_id": "provider-unique-tx-id-2",
"amount": 450,
"currency": "EUR",
"player_id": "player-123",
"provider_code": "truelabs",
"finished": true,
"session_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}

Both are signed with your callback_secret exactly like any other callback — verify X-SIGNATURE against the raw body before processing (Signature verification).


No change from the standard contract: use transaction_id as your idempotency key (treat it as an opaque string) and return the same response on a duplicate. A retried free-round settlement carries the same transaction_id, so it de-duplicates naturally. See Idempotency.


Complete these in addition to the standard wallet-callback go-live checklist.

  • Your handler reads is_free on every callback and branches on it.
  • A bet with is_free: true does not debit the player’s real-money balance.
  • A win with is_free: true credits the player (as cash, or as bonus funds per your promo rules).
  • You do not require a preceding free bet before accepting a free win (some free rounds settle with only a win).
  • You handle a free win with amount: 0 (a losing free-round batch still settles).
  • Idempotency keys off transaction_id for free rounds too (treat it as opaque).
  • You verified the signature on a free-round callback with a known body + secret pair.