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 game — games: [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.

Before running this example, choose an authorized player, supported game/stake, and valid grant dates. Generate one request ID for that logical grant, persist it as AGGREGATOR_GRANT_REQUEST_ID, and reuse it with the same body for retries. A new grant needs a new ID.

Terminal window
curl -X POST https://api.aggregator.gg/v1/free-rounds \
--header @- \
-H "Idempotency-Key: ${AGGREGATOR_GRANT_REQUEST_ID:?Set the persisted logical grant request ID}" \
-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"
}' <<EOF
Authorization: Bearer ${AGGREGATOR_API_KEY:?Set the issued API key in the protected environment}
EOF
  • 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: true — no free bet is sent. A zero-win round still settles (you may receive a win with amount: 0).

Process each callback by integration/organization, provider_code and transaction_id, applying is_free per movement. 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).


Use integration/organization, provider_code and transaction_id as the wallet identity; treat provider IDs as opaque, not globally unique. Atomically commit the wallet change and stored original status and body. Replay that result only for a duplicate of the full identity, without another wallet movement. See Wallet 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).
  • Free rounds use integration/organization, provider_code and transaction_id for idempotency too; identical IDs from different providers remain distinct.
  • You verified the signature on a free-round callback with a known body + secret pair.