# Rewards

When a player wins a reward that you pay out — cash, free spins, a jackpot — GA Promo tells your
backend with a signed `POST`. Items, points and other in-promotion prizes stay inside GA Promo and
need nothing from you.

## 9.1 The notification

```
POST <your reward endpoint>
Content-Type: application/json
X-Promo-Timestamp: <unix seconds>
X-Promo-Signature: <signature>
```

```json
{
  "delivery_id": "0199a7f2-5c1e-7c3a-9d41-2b7e8f0a1c55",
  "player_ref": "u-123",
  "player_id": "0198a1cb-4f37-7ee8-8e7d-08d39925aec0",
  "operator_id": "<the GA operator id of the player's brand>",
  "reward_type": "freespins",
  "reward_value": "20",
  "currency": "EUR",
  "reference_id": "<campaign id>",
  "timestamp": "2026-09-24T12:00:00Z"
}
```

| Field | Meaning |
| :--- | :--- |
| `delivery_id` | this one grant; **credit once per `delivery_id`** |
| `player_ref` | your id of the player — the value your events carry as `player_ref` (§5.1); present for every player your events have named, absent otherwise |
| `player_id` | the same player's id in GA |
| `operator_id` | the GA operator id of the player's brand |
| `reward_type` | `cash`, `freespins` or `jackpot` |
| `reward_value` | a decimal string: the amount in `currency` for `cash` and `jackpot`, the number of spins for `freespins` |
| `currency` | the currency of the amount |
| `reference_id` | the campaign that granted it; the same for every grant of that campaign — not a key |
| `timestamp` | when the notification was built |

## 9.2 Checking it

```
expected = lowercase_hex( HMAC-SHA256( secret, X-Promo-Timestamp + "." + raw_body ) )
```

Compare `expected` with `X-Promo-Signature` in constant time, over the exact bytes you received.
Refuse a timestamp more than 5 minutes away from your clock. The secret is the one we send you
when we connect your reward endpoint (§9.3, §10.2). A request also carries `X-Promo-Signature-Raw`, an older signature over the body alone;
do not rely on it.

## 9.3 Answering and retries

- Answer `200`, `202` or `204` once the reward is credited or recorded to credit. Anything else, or
  no answer within 10 seconds, is a failure.
- A failed notification is sent again with the same `delivery_id` and body: 8 attempts in all, the
  pause growing from 10 seconds to at most 10 minutes. After the eighth failure we stop and resolve
  it with you by hand.
- A repeat of a `delivery_id` you already credited must answer `200` and credit nothing.

You tell us the endpoint URL in the form (`rewards.endpoint_url`); each environment has its own URL
and its own secret. The secret belongs to that URL: we generate it when we connect the endpoint and
send it to you then, over the channel of §10.2 — so it comes only after you have given us a URL that
GA can reach (a public `https://` address; plain `http://` only in the sandbox), not with your other
credentials. Until the endpoint is connected, no reward notification is sent to you.

## 9.4 Delivery via Kafka

Instead of a webhook we can write each reward notification to a topic on your own Kafka cluster.
You give us, in place of `rewards.endpoint_url`: the broker addresses (`host:port`, TLS required),
the topic, the SASL mechanism (`SCRAM-SHA-512` or `SCRAM-SHA-256`) and a user with its password, sent
over the channel of §10.2. If you want to verify messages as in §9.2, say so and we send you a secret.

- **Topic.** You create it. We suggest `<env>.promo.rewards`, for example `ga-dev.promo.rewards`.
  We never create topics.
- **Access.** Grant our user `WRITE` and `DESCRIBE` on that topic.
- **Message.** The value is the JSON of §9.1, byte for byte. The key is `delivery_id`. With a secret,
  the headers `X-Promo-Timestamp` and `X-Promo-Signature` are the ones of §9.2, computed over the
  record value.
- **Delivery.** At least once: we write with `acks=all` and count a notification delivered when the
  cluster acknowledges it. A failed write is repeated on the schedule of §9.3 (8 attempts, 10 seconds
  to 10 minutes). The same `delivery_id` can arrive more than once: **credit once per `delivery_id`**.
- **Order.** The key is `delivery_id`, so records of one grant share a partition; order across grants
  is not guaranteed.
