Skip to main content
AvailableVerified against 76c1f85Markdown

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.

You will
  • Pick the right method for each route
  • Sign requests with PP-HMAC-SHA256 and check them against test vectors
  • Call activity and linking routes with the integration key
You need
  • Credentials from PlayerPal onboarding
  • A secret manager for your key secret or integration key
  • A clock synchronised with NTP
Time
About 10 minutes
For
Operators and game providers

Choose a method​

MethodRoutesWhat you sendStatus
HMAC request signing/v1/ingest, /v1/kpis/*, /v1/analytics/studio*Authorization: PP-HMAC-SHA256 … plus X-PP-Timestamp and X-PP-NonceAvailable
OAuth bearer tokenThe same routes as HMAC signingAuthorization: Bearer <access-token> from the client-credentials grantAvailable
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 headerAvailable
Per-partner credentials for the activity and linking routesThe integration-key routes aboveNot decided yetIn 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.

Partner serveryour gamePlayerPalverifies each requestPOST /v1/ingestIn transit
0:00 / 1:02

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.

LineValue
1HTTP method, uppercase
2Path exactly as sent, without the query string
3Query pairs sorted by key, then by value, each key and value RFC 3986 percent-encoded, joined with &. Empty if there is no query string.
4Lowercase hex SHA-256 of the exact request body bytes (the hash of the empty string for a GET)
5The X-PP-Timestamp value
6The 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.
Headers
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 _ ->
Sign a request
playerpal.sh
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.

#RequestCanonical query
1GET /v1/kpis/own?to=2026-09-30&kpi=own.rounds&from=2026-09-01from=2026-09-01&kpi=own.rounds&to=2026-09-30
2POST /v1/kpis/exports with body {"kpi":"own.rounds"} (illustrative path, not a live route)(empty)
Checkpoint

Vector 1 gives NhQFk3mISFyIhDIMHBwZ-pWBoO5pKuFk_yWDVycaOyY. Vector 2 gives 7Jd4HZfVqJj3aJTY7K09ipVJHIDQ_6qfICxscuh1-kw. Every sample on this site is tested against these values.

Vector 1 canonical string
GET
/v1/kpis/own
from=2026-09-01&kpi=own.rounds&to=2026-09-30
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
1760000000
nonce_0123456789abcdef
Vector 2 canonical string
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:

  1. The signature, against the issuer's published signing keys.
  2. The issuer (iss), audience (aud) and expiry (exp).
  3. The client ID, read from client_id (or sub), 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.

Bearer header
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.

RouteAudience 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/exchangex-integration-id for the game provider exchange; without it you get the operator result
In development

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.

Integration key headers
playerpal.sh
# 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)
GET/v1/aliases/{playerAlias}Reference →

5Ask for the right scopes

HMAC keys and OAuth clients carry scopes. A route checks its scope after authentication succeeds.

ScopeGrants
ingest.writePOST /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.

POST/v1/ingestingest.writeReference →
GET/v1/kpis/ownkpis.readReference →
GET/v1/analytics/studiostudio.readReference →

6Rotate keys without downtime

You can hold two active HMAC keys at a time. PlayerPal rotates and revokes keys for you on request.

  1. Ask PlayerPal to rotate your key. You receive a new key ID and secret, with the same scopes as the old key.
  2. The old key keeps working for 24 hours. Roll the new secret out to every worker in that window.
  3. 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.

Rotation timeline
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" }
StatusmessageCause
401partner_auth_requiredNo PP-HMAC-SHA256 or Bearer authorization header
401invalid_signatureMalformed header, unknown, expired or revoked key, or signature mismatch
401stale_timestampTimestamp missing, malformed or more than 300 seconds off
401invalid_nonceNonce missing or outside 16-128 allowed characters
401replayed_nonceNonce already used with this key
401invalid_tokenToken signature, issuer, audience or expiry failed
401unknown_clientToken is valid but the client is not registered to a partner
401hmac_not_configured, oauth_not_configuredThe scheme is not enabled in this environment; contact PlayerPal
401machine_auth_requiredIntegration key missing or wrong
403partner_scope_missingCredential lacks the route's scope