# Wallet API

## GA calls your wallet: the actions

GA sends `POST` to `https://<your-host>/v2/wallet/<action>`. Ten actions exist. Five are mandatory.

| Action | Mandatory | What GA asks |
|---|---|---|
| `get_balance` | **yes** | "How much can this player play with?" |
| `debit` | **yes** | "Take the stake for this bet." |
| `credit` | **yes** | "Pay this win." |
| `rollback` | **yes** | "Undo this earlier operation." |
| `reconcile` | **yes** | "Did this operation go through?" |
| `authenticate` | no | "This player is opening a game. Who are they and what's their balance?" |
| `debit_credit` | no | "Take the stake and pay the win in one step." |
| `close_round` | no | "This round is over. Here are the totals." |
| `settle_grant` | no (mandatory if you issue free rounds) | "This free-round package is finished. Here is the total win." |
| `notify` | no | "Something non-monetary happened." |

For an optional action you don't support, return the error envelope with `ERROR_CODE_UNSUPPORTED_ACTION` on HTTP 200 or 404. GA reads the envelope and stops calling that action. A bare 404 without the envelope is "no answer", and GA retries it.

The examples below show the envelope reduced to the fields that matter. `ts`, `operator_id`, `correlation`, and the headers are as in chapter 2. All values are illustrative.

## `get_balance`

**Return** the playable balance in the requested currency. Change nothing.

```json
{
  "request_id": "req-0101",
  "action": "get_balance",
  "payload": {
    "meta": {
      "request_id": "req-0101",
      "op_id": "op-bal-1001",
      "player_ref": "player-10428",
      "session_id": "0198a1cb-4f37-7ee8-8e7d-08d39925aec0"
    },
    "currency": "EUR"
  }
}
```
```json
{
  "status": "ok",
  "data": {
    "balance": {
      "currency": "EUR",
      "amount": 15000,
      "exponent": 2
    },
    "bonus_balance": {
      "currency": "EUR",
      "amount": 500,
      "exponent": 2
    }
  }
}
```

Refuse only for: player not found, currency mismatch, invalid request.

## `authenticate` (optional)

**When:** the player opens a game. GA sends the `launch_token` you passed at launch. **Return** who the player is, the currency, and the balance. Keep the token valid: it comes back in every wallet call of the session.

```json
{
  "request_id": "req-0102",
  "action": "authenticate",
  "payload": {
    "meta": {
      "request_id": "req-0102",
      "op_id": "op-auth-1002",
      "player_ref": "player-10428",
      "session_id": "0198a1cb-4f37-7ee8-8e7d-08d39925aec0",
      "launch_token": "lt-abcdef123456"
    }
  }
}
```
```json
{
  "status": "ok",
  "data": {
    "player_ref": "player-10428",
    "currency": "EUR",
    "balance": {
      "currency": "EUR",
      "amount": 15000,
      "exponent": 2
    },
    "bonus_balance": {
      "currency": "EUR",
      "amount": 500,
      "exponent": 2
    },
    "attributes": {
      "country": "DE",
      "nickname": "lucky_ann"
    }
  }
}
```

Refuse for: unknown or expired token (`SESSION_EXPIRED`), blocked player (`PLAYER_BLOCKED`), player not found.

## `debit`

**Do:** take the stake. **Return** your transaction id, the balance after, the amount debited.

Refuse when:

- `amount` is zero or negative: `INVALID_REQUEST`.
- the balance can't cover it: `INSUFFICIENT_FUNDS`. Final: GA doesn't resend that bet.
- the session or token is expired or missing on a call that names a session: `SESSION_EXPIRED` / `INVALID_REQUEST`.
- the player is blocked or over a limit: `PLAYER_BLOCKED` / `LIMIT_EXCEEDED`.
- the round is already closed or voided: `BUSINESS_REJECTED`.

Never refuse a **repeated** debit (Rule 1). A debit is also how a balance correction from the game provider reaches you, sometimes outside a live session. Apply it the same way.

```json
{
  "request_id": "req-0103",
  "action": "debit",
  "payload": {
    "meta": {
      "request_id": "req-0103",
      "op_id": "op-bet-48912",
      "player_ref": "player-10428",
      "session_id": "0198a1cb-4f37-7ee8-8e7d-08d39925aec0",
      "launch_token": "lt-abcdef123456",
      "round_id": "round-82721",
      "provider_ref": "spin-99182"
    },
    "money": {
      "currency": "EUR",
      "amount": 100,
      "exponent": 2
    }
  }
}
```
```json
{
  "status": "ok",
  "data": {
    "operator_tx_id": "tx-op-982103",
    "balance_after": {
      "currency": "EUR",
      "amount": 14900,
      "exponent": 2
    },
    "debited": {
      "currency": "EUR",
      "amount": 100,
      "exponent": 2
    }
  }
}
```

## `credit`

**Do:** add the win. Rule 3 applies: never refuse for session, block, or limits. **Return** your transaction id and the balance after.

- `amount` may be `0` for a losing spin that closes the round. Answer `ok` with the unchanged balance.
- `kind` says what the payout is: `CREDIT_KIND_WIN`, `_BONUS`, `_JACKPOT`, `_PROMO`, `_FREE_ROUND`, `_TOURNAMENT`, `_CASHBACK`, `_ADJUSTMENT`.
- `components[]` is an optional breakdown (`KIND_BASE`, `KIND_JACKPOT`, `KIND_PROMO`, `KIND_BONUS`, `KIND_CONTRIBUTION`). The parts sum to `money`.
- `round_finish: true` means this credit ends the round. Still accept a credit into a round you already closed.
- `campaign_ref` names the provider promo on a `FREE_ROUND` credit.

Refuse only for: player not found, currency mismatch, invalid request, idempotency conflict, unknown original, already rolled back.

```json
{
  "request_id": "req-0104",
  "action": "credit",
  "payload": {
    "meta": {
      "request_id": "req-0104",
      "op_id": "op-win-48913",
      "player_ref": "player-10428",
      "session_id": "0198a1cb-4f37-7ee8-8e7d-08d39925aec0",
      "launch_token": "lt-abcdef123456",
      "round_id": "round-82721",
      "provider_ref": "spin-99182"
    },
    "money": {
      "currency": "EUR",
      "amount": 2500,
      "exponent": 2
    },
    "kind": "CREDIT_KIND_WIN",
    "round_finish": true
  }
}
```
```json
{
  "status": "ok",
  "data": {
    "operator_tx_id": "tx-op-982104",
    "balance_after": {
      "currency": "EUR",
      "amount": 17400,
      "exponent": 2
    }
  }
}
```

## `debit_credit` (optional)

**Do:** take the stake and pay the win in one atomic step. Check the stake against the **current** balance first, exactly like a debit. If it doesn't cover `debit_money`, refuse the whole call with `INSUFFICIENT_FUNDS` and change nothing. Never apply only one half.

```json
{
  "request_id": "req-0105",
  "action": "debit_credit",
  "payload": {
    "meta": {
      "request_id": "req-0105",
      "op_id": "op-step-48914",
      "player_ref": "player-10428",
      "session_id": "0198a1cb-4f37-7ee8-8e7d-08d39925aec0",
      "launch_token": "lt-abcdef123456",
      "round_id": "round-82721",
      "provider_ref": "spin-99183"
    },
    "debit_money": {
      "currency": "EUR",
      "amount": 100,
      "exponent": 2
    },
    "credit_money": {
      "currency": "EUR",
      "amount": 300,
      "exponent": 2
    },
    "credit_kind": "CREDIT_KIND_WIN",
    "round_finish": true
  }
}
```
```json
{
  "status": "ok",
  "data": {
    "operator_tx_id": "tx-op-982105",
    "balance_after": {
      "currency": "EUR",
      "amount": 17600,
      "exponent": 2
    },
    "debited": {
      "currency": "EUR",
      "amount": 100,
      "exponent": 2
    }
  }
}
```

## `rollback`

**Do:** undo the operation named by `original_op_id`. Rule 4 applies when you don't know it. **Return** your transaction id, the balance after, and `original_found`.

- Rolling back a **debit** is always for the full stored amount: return the stake to the player.
- Rolling back a **credit** takes money back. `debit_mode` says what to do when the balance is too low. `DEBIT_MODE_REJECT` means refuse with `INSUFFICIENT_FUNDS`. GA's own rollbacks use this mode. `DEBIT_MODE_ALLOW_NEGATIVE` means go negative. `DEBIT_MODE_PARTIAL` means take what's there, down to zero, and report the taken amount in `debited`. `partial: true` marks a partial rollback of a credit.
- A second rollback of the same original returns the first answer and moves nothing.
- If you're still committing the original, answer an error envelope with `retryable: true`. GA comes back.
- If the player or currency doesn't match the original, refuse with `INVALID_REQUEST` or `CURRENCY_MISMATCH` and move nothing.

Never refuse a rollback for session, block, or limits. Any final refusal you do send (for example `INSUFFICIENT_FUNDS` under `DEBIT_MODE_REJECT`) stops GA's retries at once and puts the operation into manual review on GA's side.

```json
{
  "request_id": "req-0106",
  "action": "rollback",
  "payload": {
    "meta": {
      "request_id": "req-0106",
      "op_id": "op-rb-48915",
      "player_ref": "player-10428",
      "session_id": "0198a1cb-4f37-7ee8-8e7d-08d39925aec0",
      "launch_token": "lt-abcdef123456",
      "round_id": "round-82721",
      "provider_ref": "spin-99182"
    },
    "original_op_id": "op-bet-48912",
    "original_kind": "ORIGINAL_KIND_DEBIT",
    "money": {
      "currency": "EUR",
      "amount": 100,
      "exponent": 2
    }
  }
}
```
```json
{
  "status": "ok",
  "data": {
    "operator_tx_id": "tx-op-982106",
    "balance_after": {
      "currency": "EUR",
      "amount": 17700,
      "exponent": 2
    },
    "original_found": true
  }
}
```

Unknown original: same shape with `"original_found": false`, no money moved, and the id stored as rolled back.

## `close_round` (optional)

**Do:** record that the round is over. `net_win`, `bet_total`, `win_total` are for your reports. No money moves on this call.

```json
{
  "request_id": "req-0107",
  "action": "close_round",
  "payload": {
    "meta": {
      "request_id": "req-0107",
      "op_id": "op-close-48916",
      "player_ref": "player-10428",
      "round_id": "round-82721"
    },
    "net_win": {
      "currency": "EUR",
      "amount": 200,
      "exponent": 2
    },
    "bet_total": {
      "currency": "EUR",
      "amount": 100,
      "exponent": 2
    },
    "win_total": {
      "currency": "EUR",
      "amount": 300,
      "exponent": 2
    }
  }
}
```
```json
{
  "status": "ok",
  "data": {
    "operator_tx_id": "tx-op-982107",
    "balance_after": {
      "currency": "EUR",
      "amount": 17700,
      "exponent": 2
    }
  }
}
```

## `reconcile`

**When:** GA got no clear answer to an operation and asks what happened. **Do:** look up `original_op_id` and report. **Change nothing and remember nothing.** This is a read. Never return `UNKNOWN_ORIGINAL` here.

| Your record of `original_op_id` | Return `state` |
|---|---|
| not there | `RECONCILE_STATE_NOT_APPLIED` |
| still being committed | `RECONCILE_STATE_UNKNOWN` |
| applied | `RECONCILE_STATE_APPLIED` with the original `operator_tx_id` and `balance_after` |
| rolled back, or remembered as rolled back (Rule 4) | `RECONCILE_STATE_NOT_APPLIED` |

```json
{
  "request_id": "req-0110",
  "action": "reconcile",
  "payload": {
    "meta": {
      "request_id": "req-0110",
      "op_id": "op-rec-48918",
      "player_ref": "player-10428"
    },
    "original_op_id": "op-bet-48912"
  }
}
```
```json
{
  "status": "ok",
  "data": {
    "state": "RECONCILE_STATE_APPLIED",
    "operator_tx_id": "tx-op-982103",
    "balance_after": {
      "currency": "EUR",
      "amount": 14900,
      "exponent": 2
    }
  }
}
```

## `settle_grant` (mandatory if you issue free rounds)

**When:** a grant you issued with `issue_grant` (§4.4) is finished: exhausted, expired, or cancelled with a win. **Do:** credit `total_win` to the player, once per `grant_id`. The call is idempotent by `op_id`. GA sent no debits and no credits for the rounds, so this call is the only place the win reaches the player. `total_win: 0` needs only an `ok`. Exception (Money Path Rules §2.10 item 7): a win of a round played before the grant ended can reach GA after the grant ended (settled via `settle_grant`, or cancelled or expired before payout). Such a win arrives as a separate `credit` with `kind = CREDIT_KIND_FREE_ROUND` and `campaign_ref` set to the `grant_id`, idempotent by `op_id` like any other credit. A new round reported on a grant that has already ended is still refused to GA and never reaches you.

Two free-round mechanisms exist and they never overlap for the same win:

| Mechanism | Who starts it | How the win reaches your wallet |
|---|---|---|
| Grant you issue (`issue_grant`) | you | one `settle_grant` with `total_win` at the end |
| Promo free spins from the game provider | the provider or a GA campaign | an ordinary `credit` with `CREDIT_KIND_FREE_ROUND` and `campaign_ref` per winning spin |

Free-round wins may arrive with no `session_id` and no `launch_token`. Accept them (Rule 3).

```json
{
  "request_id": "req-0108",
  "action": "settle_grant",
  "payload": {
    "meta": {
      "request_id": "req-0108",
      "op_id": "op-grant-7781",
      "player_ref": "player-10428"
    },
    "grant_id": "grant-7781",
    "total_win": {
      "currency": "EUR",
      "amount": 1250,
      "exponent": 2
    },
    "rounds_played": 20
  }
}
```
```json
{
  "status": "ok",
  "data": {
    "operator_tx_id": "tx-op-982108",
    "balance_after": {
      "currency": "EUR",
      "amount": 18950,
      "exponent": 2
    }
  }
}
```

## `notify` (optional)

**Do:** record the event and answer `ok`. Nothing to move. GA sends it once and doesn't retry.

Kinds: `NOTIFY_KIND_FREE_ROUNDS_STARTED`, `_TOKEN_RESET`, `_SESSION_EXPIRED`, `_REALITY_CHECK`, `_PRIZE_AWARDED`, `_FREESPIN_ISSUED`, `_FREESPIN_FINISHED`, `_BONUS_BUY_FINISHED`.

```json
{
  "request_id": "req-0109",
  "action": "notify",
  "payload": {
    "meta": {
      "request_id": "req-0109",
      "op_id": "op-ntf-48917",
      "player_ref": "player-10428",
      "session_id": "0198a1cb-4f37-7ee8-8e7d-08d39925aec0"
    },
    "kind": "NOTIFY_KIND_SESSION_EXPIRED",
    "data": {
      "attributes": {
        "reason": "idle_timeout"
      }
    }
  }
}
```
```json
{
  "status": "ok",
  "data": {
    "operator_tx_id": "ntf-op-1"
  }
}
```

## Reference

- **POST** `/v2/wallet/get_balance` — [Get player balance: parameters, errors, code samples, try it](/docs/aggregation/reference/wallet/#tag/wallet/POST/v2/wallet/get_balance)
- **POST** `/v2/wallet/authenticate` — [Authenticate player session: parameters, errors, code samples, try it](/docs/aggregation/reference/wallet/#tag/wallet/POST/v2/wallet/authenticate)
- **POST** `/v2/wallet/debit` — [Debit player balance (bet): parameters, errors, code samples, try it](/docs/aggregation/reference/wallet/#tag/wallet/POST/v2/wallet/debit)
- **POST** `/v2/wallet/credit` — [Credit player balance (win/bonus): parameters, errors, code samples, try it](/docs/aggregation/reference/wallet/#tag/wallet/POST/v2/wallet/credit)
- **POST** `/v2/wallet/debit_credit` — [Atomic debit and credit: parameters, errors, code samples, try it](/docs/aggregation/reference/wallet/#tag/wallet/POST/v2/wallet/debit_credit)
- **POST** `/v2/wallet/rollback` — [Rollback transaction: parameters, errors, code samples, try it](/docs/aggregation/reference/wallet/#tag/wallet/POST/v2/wallet/rollback)
- **POST** `/v2/wallet/close_round` — [Close game round: parameters, errors, code samples, try it](/docs/aggregation/reference/wallet/#tag/wallet/POST/v2/wallet/close_round)
- **POST** `/v2/wallet/settle_grant` — [Settle free-round grant: parameters, errors, code samples, try it](/docs/aggregation/reference/wallet/#tag/wallet/POST/v2/wallet/settle_grant)
- **POST** `/v2/wallet/reconcile` — [Reconcile uncertain transaction: parameters, errors, code samples, try it](/docs/aggregation/reference/wallet/#tag/wallet/POST/v2/wallet/reconcile)
- **POST** `/v2/wallet/notify` — [Non-financial notification: parameters, errors, code samples, try it](/docs/aggregation/reference/wallet/#tag/wallet/POST/v2/wallet/notify)
