# Gameplay events

Rounds of games that run through GA's aggregation come from GA: never send them (see the note
before §1). This chapter and §6–§7 cover account events and the rounds of games not on GA.

## 5.1 Identifiers

| Field | What it is | Rule |
| :--- | :--- | :--- |
| `brand` | your brand | the code of the brand as registered for you in GA; we confirm the list at onboarding. **Required** when one connection carries events of several brands; **omitted** when it carries one brand |
| `player_ref` | your player | the player's id on your side — **the same value as `external_player_id` in your player tokens** (§3.2). Up to 255 bytes of UTF-8. Another id splits one person into two players |
| `event_id` | the event | yours; unique **forever** within your feed; a resend of the same event carries the same `event_id` |
| `round_id` | the round | the same for every event of one round |
| `game.id` | the game | the id of the game in the catalogue you send us (§8.1) |

All your transports share one space of `event_id`s and `round_id`s unless you tell us otherwise in
the form: an event sent through two of them is one event, and a refund sent by webhook reverses the
bet of the same round sent over Kafka.

### Bonus-money sessions: `promo_player_ref`

Some operators run bonus money under a separate session, for example `b7~1001~55`, while the
player's promotions should count for `b7~1001`. Pass the attribute `promo_player_ref` in
`LaunchGame.attributes`, set to the player's normal `player_ref` (1 to 255 characters, no
whitespace). GA Promo then credits the round to that player instead of the bonus session's player.

Only promo accounting changes: wallet calls, balance and the session's own `player_ref` stay as they
are. The ref must belong to the same operator or brand as the session; an invalid value makes the
launch fail with `InvalidArgument`.

## 5.2 The event: `promo.event.v1`

JSON Schema: [`schema/promo-event-v1.schema.json`](/docs/gamification/pack/schema/promo-event-v1.schema.json). One event is
one JSON object; the webhook also takes a batch `{"events": [ … ]}` (§6.1).

```json
{
  "spec": "promo.event.v1",
  "event_id": "01J8Z3K9T6-bet-778812",
  "brand": "partner-sandbox-eu",
  "player_ref": "u-123",
  "kind": "bet",
  "round_id": "r-99812",
  "game": {
    "id": "book-of-x"
  },
  "amount": "1.50",
  "currency": "EUR",
  "occurred_at": "2026-09-24T12:00:00Z"
}
```

| Field | Required | Rule |
| :--- | :---: | :--- |
| `spec` | yes | exactly `promo.event.v1` |
| `event_id` | yes | non-empty string (§5.1) |
| `brand` | see §5.1 | string, up to 255 bytes of UTF-8 |
| `player_ref` | yes | non-empty string, up to 255 bytes of UTF-8 |
| `kind` | yes | a round kind — `bet`, `win`, `settled`, `refund` — or an account kind — `deposit`, `withdrawal`, `login` (§5.4) |
| `round_id` | round kinds | non-empty string; not read for account kinds |
| `game.id` | no | string; not read for account kinds; a round event without a game counts only in promotions that list no games |
| `amount` | all but `login` | **a string** with a decimal number in the **major units** of `currency` (§5.3); not read for `login` |
| `currency` | all but `login` | a code from Appendix C, any letter case; not read for `login` |
| `occurred_at` | yes | RFC 3339 with a zone (`Z` or `±hh:mm`); fractions of a second allowed |

Fields you add beyond these are ignored, at the top level and inside `game`: the format grows by
adding fields. The byte limits count UTF-8 bytes, not characters.

## 5.3 Amount

A JSON **string**: digits, optionally a dot and more digits. It is converted to minor units of the
currency **exactly**; more decimals than the currency has is refused, never rounded.

| `amount` | `currency` | Result |
| :--- | :--- | :--- |
| `"1.50"`, `"1.5"` | EUR | 1.50 EUR |
| `"100"` | JPY | 100 JPY |
| `"0.00012345"` | BTC | 0.00012345 BTC |
| `"0.00"` | EUR | a `bet` or `settled` of zero is set aside (`skipped/zero_amount`) |
| `"1.505"` | EUR | refused — three decimals, EUR has two |
| `"100.0"` | JPY | refused — JPY has no decimals |
| `1.5`, `150` (JSON numbers) | any | refused |
| `"-1.50"`, `"1e2"`, `"1,50"`, `"01.50"`, `".5"`, `"1."`, `""` | any | refused |

## 5.4 Kinds

**Round kinds** — every money operation of a game round:

| `kind` | Send when | `amount` |
| :--- | :--- | :--- |
| `bet` | a stake is placed | the stake |
| `win` | a payout is made | the payout |
| `refund` | money of a round is returned | the returned amount |
| `settled` | the round is over — **exactly one per round** | **the sum of the round's bets** |

Events of a round do not have to arrive in order. Promotions that collect items credit on `settled`
only: a feed without `settled` counts nothing there.

**A refund takes the whole round out of promotions**, whatever its amount: what was already counted
for the round's `bet` and `settled` is reversed, and a `bet` or `settled` of that round that
arrives after the refund is set aside as `skipped/round_refunded`. Send `refund` only for a round
whose stake is returned.

**Account kinds** — events of the player's account, not of a round:

| `kind` | Send when | `amount` |
| :--- | :--- | :--- |
| `deposit` | the player's deposit is credited | the deposit |
| `withdrawal` | the player's withdrawal is paid out | the withdrawal |
| `login` | the player signs in | none |

They count in promotions built on them — a login streak, a deposit or withdrawal target — and never
in the promotions of rounds: they need no `round_id` or game, take no minimum bet, and a zero
`amount` is decided, not set aside.

One file per kind is in `examples/events/` of the pack: `bet.json`, `win.json`, `refund.json` and `settled.json`
carry `brand`, for a connection of several brands; `deposit.json`, `withdrawal.json` and
`login.json` are account events; `batch.json` has no `brand`, for a connection of one brand — its
second event has an unknown `kind` and is refused, the other two are accepted.
