> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zerodash.studio/llms.txt
> Use this file to discover all available pages before exploring further.

# Free rounds

> Real money bonus rounds with a stake of zero — campaigns, bonus codes, and how they land in your wallet.

A free round lets a player spin without paying the stake, while still winning real money. It flows through your wallet like any other round — the stake is simply `0`.

<Warning>
  Free rounds are **not** [free-to-play](/launch/free-to-play). Free-to-play is a demo with virtual money and no callbacks at all. Free rounds are real money: your wallet is called, transactions are created, and winnings are credited.
</Warning>

## The model

```mermaid theme={null}
flowchart LR
    A["Campaign<br/><i>XMAS-2026</i><br/>2.50 USD × 1 round<br/>lucky-duck"] --> B["Bonus code<br/><i>CODE1234</i><br/>player1"]
    A --> C["Bonus code<br/><i>CODE5678</i><br/>player2"]
    B --> D["Player redeems<br/>in game"]
    C --> E["Player redeems<br/>in game"]
```

<Columns cols={2}>
  <Card title="Campaign" icon="bullhorn">
    Defines the offer: stake per round, currency, eligible games, validity window. One campaign, many players.
  </Card>

  <Card title="Bonus code" icon="ticket">
    Attaches the campaign to **one specific player**. One code, one player, one currency.
  </Card>
</Columns>

## Setting one up

<Steps>
  <Step title="Pick eligible games" icon="grid-2">
    Only games with `freeRound: true` in [List games](/api-reference/customer/list-games) can be included.

    ```javascript theme={null}
    const eligible = games.filter((g) => g.available && g.freeRound).map((g) => g.slug);
    ```
  </Step>

  <Step title="Create the campaign" icon="bullhorn">
    [`POST /api/v1/free-rounds/campaigns`](/api-reference/customer/create-campaign)

    ```json theme={null}
    {
      "campaignCode": "XMAS-2026",
      "campaignName": "XMAS 2026",
      "amount": 2.50,
      "currencyCode": "USD",
      "startDate": "2026-05-15T11:25:16Z",
      "endDate": "2026-08-15T11:25:16Z",
      "gameSlugs": ["lucky-duck"],
      "maxRounds": 1
    }
    ```

    `amount` is the stake redeemed in **one** round. With `maxRounds: 2`, a 5 USD campaign is worth 10 USD in total.
  </Step>

  <Step title="Issue bonus codes" icon="ticket">
    [`POST /api/v1/free-rounds/campaigns/codes`](/api-reference/customer/create-bonus-code), once per player.

    ```json theme={null}
    {
      "campaignCode": "XMAS-2026",
      "userId": "acct_8f2c19",
      "currencyCode": "USD",
      "bonusCode": "CODE1234",
      "maxRounds": 1
    }
    ```

    <Warning>
      `userId` must be the exact `accountId` your [Player authorization](/api-reference/callbacks/player) callback returns, and `currencyCode` must match the campaign currency.
    </Warning>
  </Step>

  <Step title="Let the player redeem" icon="play">
    The player enters the code in the game. Each spin produces a debit and a credit in your wallet.
  </Step>

  <Step title="Track them" icon="magnifying-glass">
    [`POST /api/v1/free-rounds/campaigns/search`](/api-reference/customer/search-campaigns) lists your campaigns with pagination, and reports `isDisabled` for any that were switched off.
  </Step>
</Steps>

## maxRounds

`maxRounds` says how many rounds one bonus code grants. Set it on the campaign, on the bonus code, or both.

| Campaign | Bonus code | Result                                 |
| -------- | ---------- | -------------------------------------- |
| `2`      | omitted    | 2 rounds — inherited from the campaign |
| `2`      | `5`        | 5 rounds — the code overrides          |
| omitted  | `3`        | 3 rounds                               |
| omitted  | omitted    | **Rejected** — the code request errors |

Every round redeems the full campaign `amount`. `5 USD × 2 rounds = 10 USD` in total exposure per code.

## In your wallet

Each free spin is a **separate bet**: its own debit, its own credit, its own `transactionId`.

```mermaid theme={null}
sequenceDiagram
    participant Z as Zero-Dash
    participant O as Your wallet

    Z->>O: POST /debit — stake 0.00, reason "freeround"
    Note right of O: Deduct nothing.<br/>Still create the transaction.
    O-->>Z: 200 balance unchanged
    Z->>O: POST /credit — payout 4.85, reason "freeround"
    Note right of O: Stake already deducted<br/>from the payout.
    O-->>Z: 200 balance + 4.85
```

Both callbacks carry `reason: "freeround"` and a `freeRoundData` object:

```json theme={null}
{
  "reason": "freeround",
  "freeRoundData": {
    "campaign": "freeround-campaign-1",
    "value": { "amount": 1.00, "currency": "USD" }
  }
}
```

<ResponseField name="freeRoundData.campaign" type="string">
  The campaign the round belongs to. Use it to attribute bonus cost in your reporting.
</ResponseField>

<ResponseField name="freeRoundData.value" type="object">
  The nominal value of one free round, as configured on the campaign. Informational — it is **not** an amount to move.
</ResponseField>

### What your handlers must do

<CardGroup cols={2}>
  <Card title="Debit — stake is 0" icon="circle-minus">
    Deduct nothing. **Still create the transaction** and return a valid response with the unchanged balance. Skipping the row breaks the settlement that follows.
  </Card>

  <Card title="Credit — pay exactly what is sent" icon="circle-plus">
    The stake is already deducted from `payout`. Credit the amount given, no adjustment.
  </Card>
</CardGroup>

<Warning>
  A losing free round still sends a credit, with `payout.amount` of `0`. Same rule as a paid round — see [Wallet integration](/wallet/overview).
</Warning>

## Currency, games and windows

<AccordionGroup>
  <Accordion title="Currency must match everywhere" icon="coins">
    Campaign currency, bonus code currency and the player's account currency are one value. A player with a EUR account cannot hold a code from a USD campaign — issue a separate campaign per currency.
  </Accordion>

  <Accordion title="Only free-round-capable games" icon="grid-2">
    Every slug in `gameSlugs` must have `freeRound: true`. Check the catalogue before creating the campaign rather than debugging the rejection.
  </Accordion>

  <Accordion title="The window is checked at redemption" icon="calendar">
    `startDate` and `endDate` bound when codes can be redeemed. Issuing a code outside its campaign window creates something the player cannot use.
  </Accordion>

  <Accordion title="Bonus codes are unique and yours to generate" icon="ticket">
    You choose the code string. Make it unique across your platform — collisions between campaigns are your responsibility, not something we can disambiguate.
  </Accordion>
</AccordionGroup>

## Searching campaigns

```json theme={null}
{
  "page": 1,
  "perPage": 100,
  "currencyCode": "USD",
  "campaignCode": "XMAS",
  "from": "2026-05-15T11:25:16Z",
  "to": "2026-08-15T11:25:16Z"
}
```

<Note>
  `campaignCode` and `campaignName` match on **substring**, not exact equality. Searching `XMAS` also returns `XMAS-2026-VIP`. Filter client side when you need an exact hit.
</Note>

`from` and `to` bound the campaign's own dates: `from` returns campaigns whose `startDate` is on or after it, `to` returns campaigns whose `endDate` is on or before it. `perPage` accepts 1 to 100.
