Skip to content

Open app

Free rounds

Free rounds (free spins) let a player spin without staking their own cash: the stake is funded by a grant you issue, and the winnings are credited to the player. There are two sides, and only one of them is new work. Issuing is an API call, POST /v1/free-rounds. Receiving happens on the callback endpoint you already built: free spins arrive as ordinary bet and win callbacks carrying is_free: true, and there is no new endpoint to implement. If your wallet endpoint is not built yet, start with Callbacks.

The one rule that matters most: never debit the player’s real-money balance for a bet callback where is_free is true. The grant funds the stake; the amount on a free bet is the nominal stake for your reporting, not a charge. A naive integration that debits it silently takes real money from players on every free spin.

Grant a player free rounds on one or more games. Generate and persist one unique AGGREGATOR_OPERATION_ID, then reuse it and the identical body only for retries. Replace the sample game and dates with enabled games and the approved campaign window; the dates below are illustrative. Platforms must also send the brand’s sub_operator_ref:

Terminal window
curl -X POST https://api.aggregator.gg/v1/free-rounds <<EOF \
--header @- \
-H "Idempotency-Key: ${AGGREGATOR_OPERATION_ID:?Set a persisted unique ID for this grant; reuse only for its retries}" \
-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-11-01T00:00:00Z",
"end_at": "2026-11-10T00:00:00Z"
}'
Authorization: Bearer ${AGGREGATOR_API_KEY:?Set the issued key in the protected environment}
EOF

The response returns a grant_id and a per-game status.

What the request means:

  • count is per game. games: [A, B] with count: 10 issues 10 rounds on each, 20 in 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.
  • Issuance is idempotent. The Idempotency-Key header is required: the same key with the same body replays the cached result, and the same key with a different body returns 409. Retry an ambiguous issuance with the same key and body and inspect the recorded grant result; never mint a fresh key to bypass an unresolved outcome. See Idempotency.
  • Availability is per provider. Free rounds must be enabled for the provider on your account first; until then issuance returns 403.

Step check: issue one grant, then repeat the exact same request with the same Idempotency-Key. The second response carries the same grant_id and no second grant appears in the list call below.

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=EUR first and branch on the mode it returns:

mode Meaning What you send
enumerated The platform holds 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 The platform has no list for this currency, so stakes is empty. Send an appropriate positive bet_amount in the documented units; the provider validates it at issue time and the platform mirrors its accept or reject.
unsupported The game has no free-round stake model. Do not issue on this game.

bet_amount is a decimal string in major units, unlike callback amounts in integer minor units. Do not reuse the wallet conversion rule here. Mobule requires bet_amount plus explicit positive integer provider_params.betlevel and provider_params.rate; do not derive those two integers from the amount.

Step check: for one game of each provider you use, the stakes call returns a mode your issuing code has a branch for, and an issuance with a listed stake succeeds.

  • List grants with GET /v1/free-rounds.
  • Cancel the unplayed remainder of a grant with POST /v1/free-rounds/{grant_id}/cancel.

Cancellation is provider-dependent: inspect the HTTP result and each game’s state. Already activated or played rounds can still settle after a cancel or expiry. Mobule activates a game’s grant after launch and cannot cancel that game once activated. Unsupported cancellation can return 501; failed or ambiguous cancellation is not proof that no liability remains.

Step check: issue a small grant, cancel it, and verify the returned per-game states against the list call and provider evidence. Account for already activated games and outstanding wins rather than expecting all callbacks to stop.

A free-round callback is the standard callback payload plus three 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 this spin belongs to; use it to group a campaign’s activity. May be empty when the spin is not tied to a specific grant.
free_round_kind string or null The kind you set when issuing: freespin, freeticket, or voucher.

action, transaction_type, transaction_id, amount, currency, player_id, provider_code, and session_id keep their normal callback meanings; amount is an integer in minor units, while identifiers and action fields are strings. Branch on transaction_type plus is_free, never on action. Signatures do not change either: every free-round callback carries X-SIGNATURE over the raw body, verified the same way as always.

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 under your own wagering rules.
bet or win absent or false Ordinary cash play: debit or credit as specified in Callbacks.

Apply this table inside the complete wallet settlement boundary: authenticate raw bytes, validate trusted player/session context, atomically deduplicate and commit the movement with its original response, then return HTTP 200. A simple if/else balance update is not a complete callback handler. Refunds reverse only a recorded debit; a free bet does not justify a new cash credit.

The net effect of a correctly handled free round: the player risks nothing and keeps the winnings. Whether free-round winnings land as withdrawable cash or as restricted bonus funds subject to wagering is entirely your promotional accounting; Aggregator.gg imposes no model, and crediting them as cash is valid.

Step check: run one free-round campaign against a test player and confirm the real-money balance never decreased, every free win credited, and your reporting shows the nominal stakes under the grant_id.

The callback sequence varies by game, and your handler must cope with both shapes:

  • Both legs. A free bet (is_free: true, nominal stake) followed, on a winning spin, by a free win (is_free: true).
  • Win only. A single free win carrying the total winnings, with finished: true and no free bet before it. A zero-win batch still settles: you may receive a free win with amount: 0.

Use trusted integration/organization context, provider_code and transaction_id as the persistent identity, with is_free deciding the wallet effect. Do not require a matching free bet before accepting a free win. A retried free-round settlement carries the same transaction_id, so your standard deduplication covers it with no extra work.

A bet leg from a 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"
}

Before a free-round campaign goes to real players, in addition to the standard 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.
  • You handle a free win with amount: 0.
  • Your callback identity combines trusted integration/organization context, provider_code and opaque transaction_id, with atomic movement and response storage.
  • You verified the signature on a free-round callback with a known body and secret pair.

For Mobule, issuance stores a pull-activated grant without an immediate provider call. A later supported launch carries the grant reference, and completion delivers a free win, including an eligible late win after expiry or cancellation. Check provider approval and environment enablement: a launch with free rounds disabled can be ordinary paid play even if an earlier grant exists. See the issue contract before launching a campaign.