Authenticate API calls
Every partner call to PlayerPal is authenticated as your organisation, never as a player. Your credential decides which party you are, which routes you can call and which data you see. A party ID sent in a query string or an x-party-id header is ignored on partner-credential routes: your scope always comes from the credential.
Choose a method
| Method | Routes | What you send | Status |
|---|---|---|---|
| HMAC request signing | /v1/ingest, /v1/kpis/*, /v1/analytics/studio* | Authorization: PP-HMAC-SHA256 … plus X-PP-Timestamp and X-PP-Nonce | Available |
| OAuth bearer token | The same routes as HMAC signing | Authorization: Bearer <access-token> from the client-credentials grant | Available |
| Integration key | /v1/activity-batches, /v1/activity-uploads, /v1/link-intents, /v1/link-results/exchange, /v1/aliases/{playerAlias} | x-api-key: <your-integration-key> plus an audience header | Available |
| Per-partner credentials for the activity and linking routes | The integration-key routes above | Not decided yet | In development |
HMAC signing and OAuth are interchangeable: use whichever PlayerPal registered for you.
1Sign requests with PP-HMAC-SHA256
Watch first. This walks through the same steps as the text below, using the test-vector values.
The problem
A request crosses the internet, and anyone on the path could change it. PlayerPal needs to know it came from you, unchanged.
PlayerPal issues you a key ID (pp_kid_…) and a secret (pp_sk_…). The secret is shown once, so store it in your secret manager. Every signed request carries three headers.
The signature is an HMAC-SHA256 of a canonical string: six lines joined with \n, with no trailing newline.
| Line | Value |
|---|---|
| 1 | HTTP method, uppercase |
| 2 | Path exactly as sent, without the query string |
| 3 | Query pairs sorted by key, then by value, each key and value RFC 3986 percent-encoded, joined with &. Empty if there is no query string. |
| 4 | Lowercase hex SHA-256 of the exact request body bytes (the hash of the empty string for a GET) |
| 5 | The X-PP-Timestamp value |
| 6 | The X-PP-Nonce value |
Then signature = base64url_nopad(HMAC-SHA256(secret, canonical)).
- The timestamp is Unix seconds and must be within 300 seconds of PlayerPal's clock, in either direction.
- The nonce is 16 to 128 characters from
A-Z a-z 0-9 _ -. Generate a fresh random one per request. - A nonce can be used once per key within the window. A repeat is rejected as
replayed_nonce. The nonce is only recorded after the signature checks out, so a forged request cannot use up your nonces. - Sign the body bytes you actually send. Re-serialising JSON after signing breaks the signature.
- Signatures are compared in constant time.
Authorization: PP-HMAC-SHA256 keyId=pp_kid_XXXX, signature=<base64url>
X-PP-Timestamp: <unix seconds>
X-PP-Nonce: <16-128 chars of A-Z a-z 0-9 _ ->
API="${PP_API_BASE:-}" # https://<playerpal-api-host>
KEY_ID="${PP_KEY_ID:-}" # pp_kid_…
SECRET="${PP_SECRET:-}" # pp_sk_…
# sign METHOD PATH QUERY BODY [TIMESTAMP] [NONCE]
# QUERY must already be canonical: pairs sorted by key then value, RFC 3986 encoded.
# Prints three header lines for curl.
sign() {
local method=$1 path=$2 query=$3 body=$4
local ts=${5:-$(date +%s)} nonce=${6:-$(openssl rand -hex 16)}
local body_hash sig
body_hash=$(printf '%s' "$body" | openssl dgst -sha256 -hex | awk '{print $NF}')
sig=$(printf '%s\n%s\n%s\n%s\n%s\n%s' "$method" "$path" "$query" "$body_hash" "$ts" "$nonce" \
| openssl dgst -sha256 -hmac "$SECRET" -binary | openssl base64 -A | tr '+/' '-_' | tr -d '=')
printf 'Authorization: PP-HMAC-SHA256 keyId=%s, signature=%s\nX-PP-Timestamp: %s\nX-PP-Nonce: %s\n' \
"$KEY_ID" "$sig" "$ts" "$nonce"
}
2Check your signer against the test vectors
Run your signer with secret pp_sk_test-vector-secret, timestamp 1760000000 and nonce nonce_0123456789abcdef. The vectors test the signing algorithm only, not a live call.
| # | Request | Canonical query |
|---|---|---|
| 1 | GET /v1/kpis/own?to=2026-09-30&kpi=own.rounds&from=2026-09-01 | from=2026-09-01&kpi=own.rounds&to=2026-09-30 |
| 2 | POST /v1/kpis/exports with body {"kpi":"own.rounds"} (illustrative path, not a live route) | (empty) |
Vector 1 gives NhQFk3mISFyIhDIMHBwZ-pWBoO5pKuFk_yWDVycaOyY. Vector 2 gives 7Jd4HZfVqJj3aJTY7K09ipVJHIDQ_6qfICxscuh1-kw. Every sample on this site is tested against these values.
GET
/v1/kpis/own
from=2026-09-01&kpi=own.rounds&to=2026-09-30
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
1760000000
nonce_0123456789abcdef
POST
/v1/kpis/exports
4dd0775e21e5aa993d44025a43efbd7aa90f24f91b463afdb3e80787704582df
1760000000
nonce_0123456789abcdef
3Or use an OAuth access token
If PlayerPal registered an OAuth client for your organisation, it is mapped to your party and scopes. Request an access token from the identity provider with the client-credentials grant, using the token endpoint and audience PlayerPal gives you during onboarding. The token endpoint belongs to the identity provider, not to the PlayerPal API.
PlayerPal verifies each token as follows:
- The signature, against the issuer's published signing keys.
- The issuer (
iss), audience (aud) and expiry (exp). - The client ID, read from
client_id(orsub), must belong to an active client registered to a partner.
Tokens are short-lived (5 to 15 minutes). Cache one per worker and refresh it before it expires, rather than fetching one per request. Signed requests and bearer tokens reach the same routes with the same scopes.
GET /v1/kpis/catalogue
Authorization: Bearer <access-token>
4Call activity and linking routes with the integration key
The activity and linking routes use an integration key issued by PlayerPal. Send it as x-api-key: <your-integration-key> or Authorization: Bearer <your-integration-key>. These routes take the audience from a header, never from the request body.
| Route | Audience header |
|---|---|
POST /v1/activity-batches, GET /v1/activity-batches/{batchId}, /v1/activity-uploads* | x-integration-id (required) |
POST /v1/link-intents, GET /v1/aliases/{playerAlias} | x-operator-id (operators) or x-integration-id (game providers) |
POST /v1/link-results/exchange | x-integration-id for the game provider exchange; without it you get the operator result |
Per-partner credentials for the activity and linking routes are being built. Until then, treat the integration key like any production secret: keep it server-side and never ship it to a browser or app.
# Linking and activity routes use the integration key PlayerPal issued with
# your integration, plus the ID of the operator or integration you act for.
INTEGRATION_KEY="${PP_INTEGRATION_KEY:-}"
OPERATOR_ID="${PP_OPERATOR_ID:-}" # pp_opr_… (operators)
INTEGRATION_ID="${PP_INTEGRATION_ID:-}" # pp_int_… (game providers)
5Ask for the right scopes
HMAC keys and OAuth clients carry scopes. A route checks its scope after authentication succeeds.
| Scope | Grants |
|---|---|
ingest.write | POST /v1/ingest, GET /v1/ingest/batches/{batchId} |
kpis.read | /v1/kpis/* |
studio.read | /v1/analytics/studio* (game providers only) |
An HMAC key issued without named scopes gets kpis.read only. Ask for ingest.write if you send data. The integration key has no scopes; its routes are limited by the audience header instead.
6Rotate keys without downtime
You can hold two active HMAC keys at a time. PlayerPal rotates and revokes keys for you on request.
- Ask PlayerPal to rotate your key. You receive a new key ID and secret, with the same scopes as the old key.
- The old key keeps working for 24 hours. Roll the new secret out to every worker in that window.
- After 24 hours the old key stops working, and requests signed with it fail with
invalid_signature.
Rotation needs a free slot: if you already hold two active keys, one is revoked first. A revoked key stops working immediately, so ask for a revocation, not a rotation, if a secret leaks.
t = 0 new key issued, old key still active
t < 24 h both keys accepted; deploy the new secret
t = 24 h old key expires; only the new key works
Errors
Auth failures use the standard error body, with the code in message. Do not retry them unchanged: fix the credential, clock or signer first.
{ "statusCode": 401, "message": "stale_timestamp", "error": "Unauthorized" }
| Status | message | Cause |
|---|---|---|
401 | partner_auth_required | No PP-HMAC-SHA256 or Bearer authorization header |
401 | invalid_signature | Malformed header, unknown, expired or revoked key, or signature mismatch |
401 | stale_timestamp | Timestamp missing, malformed or more than 300 seconds off |
401 | invalid_nonce | Nonce missing or outside 16-128 allowed characters |
401 | replayed_nonce | Nonce already used with this key |
401 | invalid_token | Token signature, issuer, audience or expiry failed |
401 | unknown_client | Token is valid but the client is not registered to a partner |
401 | hmac_not_configured, oauth_not_configured | The scheme is not enabled in this environment; contact PlayerPal |
401 | machine_auth_required | Integration key missing or wrong |
403 | partner_scope_missing | Credential lacks the route's scope |