8.1 Your catalogue → GA Promo
If your games are not on GA, GA Promo learns them from you: promotions are set up on your games and an event is checked against them. It holds only games that do not run through GA: GA’s games reach GA Promo with GA’s own rounds, and a copy of GA’s catalogue (§8.2) is not sent here.
Sending it
POST https://<api host>/promo/v1/catalog/<source_id>
Content-Type: application/json
X-Promo-Timestamp: <unix seconds>
X-Promo-Signature: <signature>
The URL takes the source_id of your webhook transport, and the request is signed exactly like its events (§6.1): the same secret, the same ±300 s window. A catalogue cannot be sent over Kafka or RabbitMQ.
{
"mode": "snapshot",
"games": [
{
"id": "book-of-x",
"name": "Book of X",
"provider": "Acme Studio",
"category": "Slots",
"image_url": "https://cdn.acme.example/book-of-x.png"
},
{
"id": "crash-9",
"name": "Crash 9",
"category": "Crash",
"is_active": false
}
]
}
mode | List | Effect |
|---|---|---|
snapshot | games | your catalogue becomes exactly games, at once; a game not listed is removed; [] clears it |
upsert | games | the listed games are added or updated; the others stay |
delete | game_ids | the listed games are removed; unknown ids are ignored |
| Game field | Required | Rule |
|---|---|---|
id | yes | up to 255 bytes; the same string your events carry as game.id |
name | yes | not blank, up to 255 bytes |
provider | no | up to 255 bytes |
category | no | up to 255 bytes; the category of the game |
image_url | no | an absolute https URL, up to 2048 bytes |
is_active | no | true by default; an inactive game is kept but not shown and not counted |
One request carries up to 10 000 games or ids and 8 MiB. Fields you add beyond these are ignored; an id twice in one request is an error. JSON Schema:
schema/promo-catalogue-v1.schema.json; a snapshot to try is
examples/catalogue/snapshot.json, sent with the webhook examples of §6.1 by pointing
PROMO_INGEST_URL at the catalogue URL.
| Status | Body | Meaning |
|---|---|---|
200 | {"source":"<source_id>","mode":"snapshot","upserted":2,"deleted":0,"total":2} | applied; the same request again changes nothing and answers upserted: 0, deleted: 0 |
400 | {"error":"invalid","details":[{"index":0,"field":"name","reason":"required"}]} | a game breaks a rule; nothing is written. reason: required, too_long, not_a_string, not_a_boolean, not_https_url, duplicate, not_an_object |
400 | {"error":"malformed"} | not a JSON object, an unknown mode, or the list missing |
401 | {"error":"unauthorized"} | missing or stale timestamp, missing signature, or a signature that matches no secret (§6.1) |
404 | {"error":"unknown_source"} | no active webhook connection with this source_id |
413 | {"error":"body_too_large"} / {"source":"<source_id>","error":"batch_too_large"} | over 8 MiB, or more than 10 000 games or ids |
503 + Retry-After: 1 | {"error":"unavailable"} | not applied; send the same request again |
What it changes
- Your events are held to it. Once your catalogue holds a game, a
betorsettledwhosegame.idis not an active game of it is set aside asskipped/game_unknown.winandrefundare never held back, so a refund still reverses its round. Send the catalogue before a game appears in your feed: an event set aside stays set aside when the game arrives later. - Promotions are set up on your games. We pick them from your catalogue for a promotion’s game lists.
- The game widgets do not show it.
game-sectionsandgame-sectionshow GA’s catalogue for your operator, never the one you send us.
The catalogue is one per client connection: every brand of a group shares it.
8.2 GA’s catalogue → you
If your games run through GA’s aggregation, you can keep a copy of GA’s catalogue on your side — a full snapshot once, then the changes. This is optional: the widgets read the catalogue themselves, and GA Promo needs no copy of it (§8.1).
GET https://<api host>/v1/catalog/snapshot
GET https://<api host>/v1/catalog/diff?cursor=<catalog_version or next_cursor>&limit=200
X-Public-API-Key: <your public API key>
X-Public-Timestamp: <unix seconds>
X-Public-Signature: <signature>
signature = lowercase_hex( HMAC-SHA256( secret, METHOD + path_with_query + body + X-Public-Timestamp ) )
path_with_queryis the full request path with its query string, e.g./v1/catalog/diff?cursor=…&limit=200;bodyis empty for theseGETs.- The timestamp may be up to 5 minutes old and up to 30 seconds ahead of our clock.
- The key needs the
read:catalogscope; we issue it with your credentials (§10.2).
Snapshot — one consistent read: catalog_version (the cursor to store once you have saved the snapshot), generated_at, providers[], categories[], games[] and category_games[]
(category_id, game_id, display_order).
A game’s image_url is an absolute URL of its cover for your operator,
https://<api host>/public/v1/assets/games/<game id>/image?operator_id=<your GA operator id>. Every game has one: the address answers 200 with the cover, or with a placeholder marked by the response header X-Image-Fallback: true when the game has no cover yet. The same holds for payload.image_url
of a game in the diff.
Diff — changes[] in order, each entity_type, entity_id, operation, updated_at,
payload; upsert carries the whole current object, disable and delete are tombstones. Continue with next_cursor while has_more is true. limit is 1–1000, 200 by default. A cursor too old to serve answers 410 cursor_expired: take a new snapshot. A cursor works only with the key that received it.