Integration guide
Getting your first player on the ladder
There are two halves to this. You publish events to a queue so the platform knows what your players are doing, and you call the API to read their standing back and draw it into your site. The first half is one message. The second is nine endpoints, all signed the same way.
Your first event
Publish to the queue you were given at onboarding. Nothing else has to be in place first — a player who has never been seen is created by their first event.
{
"EventUid": "3f6c1a9e-8d24-4c5b-9d0e-1a2b3c4d5e6f",
"SchemaVersion": 1,
"OccurredUtc": "2026-08-28T09:14:02.481Z",
"CustomerID": "9f2c77b1",
"Action": "Bet",
"Amount": 25.00,
"Currency": "EUR",
"Provider": "Pragmatic",
"GameType": "Casino",
"GameID": "2324"
}
EventUid is yours to generate and it is how duplicates are dropped. Publish the
same uid twice — after a timeout, a retry, a redelivery — and the second one is ignored. Send
a fresh uid for a genuinely new bet, and reuse the old one for a retry of the same bet.
CustomerID is your identifier for the player. The platform never asks
you to adopt an id of ours, and never returns one.
GET /v1/customers/9f2c77b1/gamification and you should see
enrolled: true with points on the clock. That round trip — publish, then read
— is the whole integration in miniature, and it is worth doing before you write any UI.
The two first-day mistakes
These are first, not in a troubleshooting section at the bottom, because between them they account for most of the time lost on a first integration. Both produce errors that do not say what is wrong.
1. Query parameters are signed in sorted order
?status=all&limit=25 is signed as ?limit=25&status=all.
Order carries no meaning in a URL and carries a great deal in a signature, so the
specification pins it: sort by name, then encode.
Send the query in whatever order you like on the wire. Sort it only when you build the string
you sign. Getting this wrong gives you a 401 that says nothing about ordering,
and every parameterless call you tried first will have worked perfectly.
2. Writes require an Idempotency-Key
Not optional, and not only for retries. Any request that changes something needs the header:
POST /v1/customers/9f2c77b1/awards/8812/redeem Idempotency-Key: 0f6a5b2c-7d31-4e88-a0c9-b7e42f109d55
A request that times out never tells you whether it landed. Without a key, retrying it means
doing it again — redeeming a prize twice, buying twice. With one, the second attempt returns
the stored answer to the first and carries Idempotent-Replay: true so you can
tell the two apart.
Signing a request
Four headers travel on every call:
| Header | What it is |
|---|---|
X-GP-Key | Your API key. Public — it travels in the clear. |
X-GP-Timestamp | Unix seconds. Must be within 300 s of our clock. |
X-GP-Nonce | Unique per request. Remembered for 600 s, so a replay is refused. |
X-GP-Signature | Base64 HMAC-SHA256 of the canonical string. |
The canonical string is exactly five lines joined with \n — never
\r\n, which is the third mistake people make on Windows:
GET /v1/customers/CASUAL-0001/awards?limit=25&status=all 1755648060 c9f0f895fb98ab9159f51fd0297e236d e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
- Line 1 — the method, uppercase.
- Line 2 — the path, with the query sorted by name and RFC 3986 encoded.
- Line 3 — the same value you put in
X-GP-Timestamp. - Line 4 — the same value you put in
X-GP-Nonce. - Line 5 — lowercase hex SHA-256 of the raw body, byte for byte, with no reformatting. The value above is the hash of an empty body, which is what a GET uses.
The signing key is PBKDF2-SHA256(secret, salt, iterations) truncated to 64
bytes. Derive it once at startup and keep it. At 310,000 iterations it is deliberately
slow, and deriving it per request will dominate your latency.
Working clients in C#, Node, Python and PHP, and the vectors they are checked against, are
shipped in docs/examples/signing/. Ask us and we will send them across.
Reading a player
One call gives you everything a level widget needs. It is deliberately one fat endpoint instead of five tidy ones: five would mean five round trips to draw one component, and five chances for one of them to fail.
GET /v1/customers/9f2c77b1/gamification
{
"enrolled": true,
"level": { "code": "GOLD", "name": "Gold", "ordinal": 3 },
"levelPoints": 4820,
"nextLevel": { "code": "PLATINUM", "pointsRequired": 6000 },
"tokenBalance": 1250,
"awards": { "claimable": 2 }
}
enrolled: false, the entry level and zeros. To your site that player exists —
they are logged in and looking at the widget — so treating them as missing would put an
error where a starting position belongs.
Pass Accept-Language and level names and award descriptions come back in that
language where a translation exists, falling back to the merchant's default.
The life of an award
An award moves through a small number of states, and your UI needs to say something different at each one.
| State | What happened | What the player should see |
|---|---|---|
| Available | Granted and waiting to be claimed. | A button. This is the only actionable state. |
| Redeeming | Claimed; we are delivering it to your provider. | In progress. Usually seconds, occasionally longer if the provider is slow. |
| Redeemed | Delivered, or nothing to deliver. | Done, with the provider's reference where there is one. |
| Expired | Its validity window passed unclaimed. | Gone. Worth showing in history so the deadline feels real next time. |
| Failed | The provider refused it permanently. | Nothing. It is in the operator's queue for a person to deal with. |
Awards of pure token value have nothing to deliver, so they are settled the moment they are granted and go straight to Redeemed. Do not build a claim flow that assumes every award passes through Available.
What the errors mean
| Status | Meaning | What to do |
|---|---|---|
400 | The request is malformed. | Fix it. Retrying will not help. |
401 | The signature did not verify, or the timestamp is outside the window. | Check your clock first — it is more often that than the signature. Then the sorted query. |
403 | Authenticated, but not for this player or this resource. | Usually a player id belonging to another operator. |
404 | No such award, item or purchase. | Not used for players — see above. |
409 | The state moved under you: already claimed, out of stock, cap reached. | Re-read and show the player what is true now. |
429 | Rate limited. | Back off and honour Retry-After. Do not retry immediately in a loop. |
5xx | Ours. | Retry with backoff, and keep the same Idempotency-Key on writes. |
Credentials and rotation
A credential is a key, a secret, a salt and an iteration count. The key is public and travels on every request; the secret is shown once, when it is issued, and is stored only as a hash. There is no call that hands it back — if it is lost, the answer is to rotate.
Rotation issues a second credential while the first still works, so you deploy the new one and retire the old one afterwards, with no window where calls fail. Plan for two credentials to be valid at once; a client that can only hold one turns every rotation into an outage.