> For the complete documentation index, see [llms.txt](https://docs.thndr.io/integration/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.thndr.io/integration/operator-api/free-matches.md).

# Free Matches

Free matches let the Operator give a player one or more matches at a chosen tier without the player paying the entry fee. Each free match is one match: a single game against one opponent. The player plays a normal match against a paying opponent for the normal reward. THNDR does not debit the player's wallet for a free match, and reports the combined result of all matches on the grant in a single PROMO transaction once the grant has been played out or has expired.

Free matches are enabled per Operator. Contact THNDR to enable them for your integration. They are available to Operators integrated with the Transactions webhook.

#### How it works

1. The Operator grants free matches to a player: a tier, a number of matches, the games they can be played in, and an optional expiry.
2. When the player opens one of the listed games in a session launched with the grant's `brandId` (or with no `brandId`, for a grant without one), the free match appears as a tile above the regular tiers. Tapping it starts matchmaking with no payment step.
3. THNDR sends **no BET** for a free match, and no WIN, LOSE, DRAW or ROLLBACK either. Each outcome is recorded against the grant.
4. When every match on the grant has been played, or the grant expires with at least one match played, THNDR sends one PROMO transaction. The Operator credits its `amount` to the player.

Until PROMO arrives, the Operator's wallet sees nothing for a free match.

#### Grant lifecycle

| Status    | Meaning                                                                                                            |
| --------- | ------------------------------------------------------------------------------------------------------------------ |
| Active    | Matches remain and the grant has not expired. The tile is shown to the player.                                     |
| Cancelled | The Operator cancelled the grant before any match was started. Nothing is paid out.                                |
| Expired   | `expireAt` passed. If at least one match was played, PROMO is sent for the matches played. Unplayed matches lapse. |
| Settled   | PROMO has been delivered to the Operator.                                                                          |

* A match that was started before `expireAt` is allowed to finish. Expiry only stops new matches from starting.
* Expired grants are processed every few minutes, so PROMO for an expired grant can arrive a few minutes after `expireAt`.
* A grant that expires or is cancelled with no match played sends nothing.

#### What each outcome adds to the grant

Every free match is settled as if the player had paid the entry fee, so the numbers match what a paid match would have produced.

| Outcome                               | Added to `cost`       | Added to `amount`     | Match used up?              |
| ------------------------------------- | --------------------- | --------------------- | --------------------------- |
| Win                                   | the match's entry fee | the tier's reward     | Yes                         |
| Lose                                  | the match's entry fee | nothing               | Yes                         |
| Draw                                  | the match's entry fee | the match's entry fee | Yes                         |
| No opponent found, or match cancelled | nothing               | nothing               | No, it returns to the grant |

The entry fee is what a BET for the same tier would have carried as its `amount`: the tier's base fee plus the service fee. The reward is twice the base fee.

{% hint style="info" %}
**Draws.** A draw hands the entry fee back, exactly as a paid draw does. A grant of one match that ends in a draw therefore sends a PROMO with `cost` and `amount` equal.
{% endhint %}

#### Crediting winnings after every match

PROMO is sent once per grant, not once per match. A grant of five matches pays out only after the fifth match finishes, or when the grant expires.

If you want winnings credited after every match, grant **one match per `grantId`**. Each grant then settles and pays out as soon as its single match finishes.

#### Tiers

A free match is granted at a single `tierId`. The tier must be:

* A match tier. Tournament tiers cannot be granted.
* In your Operator's tier list and priced in your match currency.
* Enabled for every game listed on the grant.

THNDR provides your tier list during onboarding. As an example, the USD match tiers are:

| `tierId`   | Base fee (USD cents) | Reward (USD cents) |
| ---------- | -------------------- | ------------------ |
| `bet25`    | 25                   | 50                 |
| `bet50`    | 50                   | 100                |
| `bet100`   | 100                  | 200                |
| `bet250`   | 250                  | 500                |
| `bet500`   | 500                  | 1000               |
| `bet1000`  | 1000                 | 2000               |
| `bet2500`  | 2500                 | 5000               |
| `bet5000`  | 5000                 | 10000              |
| `bet10000` | 10000                | 20000              |
| `bet25000` | 25000                | 50000              |
| `bet50000` | 50000                | 100000             |

The grant response echoes the tier's `baseFee` and `matchCurrency`, so you can confirm exactly what was granted. The tier's fee is fixed at grant time: a later change to your tier list does not change what an existing grant is worth.

#### Currency

The grant's `currency` is the player's wallet currency. It is authoritative for the whole grant: every free match on it is played, displayed and paid out in that currency, whatever currency the player's session was launched in. Amounts are converted from the match currency with the exchange rate in force when the player's session started (see Currencies).

#### Test players

Grants to test players are accepted and play normally. Pass `isTest: true` on the grant for a player who has not launched a game yet, so the player is created as a test player. PROMO carries `isTest: true` for test players.

### Grant

Grants one or more free matches to a player.

#### Endpoints

**Sandbox**

```
POST https://operator-sandbox.thndr-api.com/v1/free-matches
```

**Production**

```
POST https://operator.thndr-api.com/v1/free-matches
```

**Request headers**

```
x-operator-id: your_operator_id
x-api-key: your_api_key
Content-Type: application/json
```

**Request body**

```json
{
  "userId": "{USER_ID}",
  "grantId": "8f14e45f-ceea-467a-9575-8b2b3c4f2a10",
  "campaignId": "welcome-2026-10",
  "currency": "EUR",
  "games": [{ "id": "solitaire" }, { "id": "blocks" }],
  "tierId": "bet100",
  "matchCount": 3,
  "expireAt": "2026-10-31T23:59:59Z"
}
```

* `userId` - Player identifier on the Operator's platform, the same value used when launching the game. A player who has never launched a game is created on grant.
* `grantId` - The Operator's own identifier for this grant, up to 64 characters. Must be unique per Operator: reusing it is refused with `DUPLICATE`. A UUID is recommended. This is the id used to cancel the grant and is returned on PROMO.
* `campaignId` - Optional. The Operator's campaign identifier, up to 64 characters. Stored for reporting only.
* `currency` - The player's wallet currency. Must be your match currency or a currency convertible from USD (see Currencies). All amounts for this grant are in this currency.
* `games` - The games the free matches can be played in, 1 to 10 entries. Each `id` is a game identifier from the Game list. The free match tile appears in each listed game.
* `tierId` - The tier to grant, from your tier list. See Tiers.
* `matchCount` - Number of free matches on this grant. Integer, minimum 1.
* `expireAt` - Optional. When unplayed matches lapse, in ISO 8601 format with a UTC offset (e.g., `2026-10-31T23:59:59Z`). Must be in the future. Defaults to 7 days after the grant.
* `subOperatorId` - Optional. Sub-operator identifier, 4 to 50 characters, as on game launch. Stored for reporting only.
* `brandId` - Optional. Brand identifier, 1 to 50 characters, as on game launch. Restricts the grant to that brand: the free match is shown and playable only in sessions launched with the same `brandId`, and PROMO is sent for that brand. Omit it for a grant meant for sessions launched without a `brandId`. Such a grant is not shown in branded sessions. If every one of your games is launched with a `brandId`, send it on every grant.
* `isTest` - Optional. Marks the player as a test player. If the player already exists with a different test status, the request is refused with `INVALID_REQUEST`. Omit it to keep the player's existing status.

{% hint style="warning" %}
The request is strict. Any field not listed above is refused with `INVALID_REQUEST.` THNDR grants by `tierId` and does not support wagering requirements, scheduled starts or per-device restrictions.
{% endhint %}

**Response**

`200 OK`

```json
{
  "tierId": "bet100",
  "baseFee": 100,
  "matchCurrency": "USD"
}
```

* `tierId` - The tier granted.
* `baseFee` - The tier's base fee in the smallest unit of the match currency, fixed for the life of the grant.
* `matchCurrency` - The currency the tier is priced in.

#### Grant errors

Errors are returned as:

```json
{
  "errors": [
    {
      "code": "DUPLICATE",
      "message": "Grant id 8f14e45f-ceea-467a-9575-8b2b3c4f2a10 already exists for your_operator_id"
    }
  ]
}
```

| HTTP | `code`            | When                                                                                                                                                                                                                                                                       |
| ---- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400  | `DUPLICATE`       | A grant with this `grantId` already exists for your Operator.                                                                                                                                                                                                              |
| 400  | `NOT_SUPPORTED`   | Free matches are not enabled for your Operator, or the tier cannot be granted: unknown, not a match tier, not in your tier list, not in your match currency, or not enabled for a listed game. Also returned when `currency` cannot be converted from your match currency. |
| 400  | `INVALID_REQUEST` | The body is malformed, a required field is missing or out of range, an unsupported field is present, a game id is unknown, `expireAt` is in the past, or `isTest` contradicts the stored player.                                                                           |

`message` is a human-readable explanation for logging. Do not parse it.

{% hint style="info" %}
**Retries.** A grant is safe to retry with the same `grantId` after a `5XX` response or a timeout. If the retry returns `DUPLICATE`, the original request succeeded and the grant exists.
{% endhint %}

### Cancel

Cancels a grant before any of its matches has been started.

#### Endpoints

**Sandbox**

```
DELETE https://operator-sandbox.thndr-api.com/v1/free-matches/{grantId}
```

**Production**

```
DELETE https://operator.thndr-api.com/v1/free-matches/{grantId}
```

**Path parameters**

| Parameter | Type   | Description                                    |
| --------- | ------ | ---------------------------------------------- |
| `grantId` | string | The `grantId` the Operator sent when granting. |

**Request headers**

```
x-operator-id: your_operator_id
x-api-key: your_api_key
```

**Response**

`204 No Content`, with an empty body, when the grant is cancelled. Cancelling a grant that is already cancelled also returns `204`, so the call is safe to retry.

A cancelled grant is removed from the player's game, and no PROMO is sent for it.

#### Cancel errors

Errors use the same format as Grant errors.

| HTTP | `code`                | When                                                                                                |
| ---- | --------------------- | --------------------------------------------------------------------------------------------------- |
| 404  | `GRANT_NOT_FOUND`     | No grant with this `grantId` exists for your Operator.                                              |
| 400  | `CANCELLATION_DENIED` | A match on the grant has already been started or played, or the grant has expired or been paid out. |

{% hint style="info" %}
Once a match has started, a grant can no longer be cancelled. It finishes normally and pays out through PROMO when it is played out or expires. A match that ended without an opponent does not count as started: it is returned to the grant, and the grant can be cancelled again if no other match was played.
{% endhint %}
