# 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.

## Sign requests with PP-HMAC-SHA256

Watch first. This walks through the same steps as the text below, using the test-vector values.

<Explainer id="signing" />

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.

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 _ ->
```

(Code sample: sign. See the page for curl, Node.js, Python, PHP, Java and C#.)

## Check 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.

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
```

## Or 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](./onboarding.mdx). 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>
```

## Call 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 |

:::info[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.
:::

(Code sample: integration-key. See the page for curl, Node.js, Python, PHP, Java and C#.)

`getAliasStatus` (see the API reference)

## Ask 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.

`ingestSubmit` (see the API reference)

`kpisOwn` (see the API reference)

`studioRollup` (see the API reference)

## Rotate 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.

```json
{ "statusCode": 401, "message": "stale_timestamp", "error": "Unauthorized" }
```

| Status | `message` | Cause |
|---|---|---|
| `401` | [`partner_auth_required`](./idempotency-and-errors.mdx#partner_auth_required) | No `PP-HMAC-SHA256` or `Bearer` authorization header |
| `401` | [`invalid_signature`](./idempotency-and-errors.mdx#invalid_signature) | Malformed header, unknown, expired or revoked key, or signature mismatch |
| `401` | [`stale_timestamp`](./idempotency-and-errors.mdx#stale_timestamp) | Timestamp missing, malformed or more than 300 seconds off |
| `401` | [`invalid_nonce`](./idempotency-and-errors.mdx#invalid_nonce) | Nonce missing or outside 16-128 allowed characters |
| `401` | [`replayed_nonce`](./idempotency-and-errors.mdx#replayed_nonce) | Nonce already used with this key |
| `401` | [`invalid_token`](./idempotency-and-errors.mdx#invalid_token) | Token signature, issuer, audience or expiry failed |
| `401` | [`unknown_client`](./idempotency-and-errors.mdx#unknown_client) | Token is valid but the client is not registered to a partner |
| `401` | [`hmac_not_configured`](./idempotency-and-errors.mdx#hmac_not_configured), [`oauth_not_configured`](./idempotency-and-errors.mdx#oauth_not_configured) | The scheme is not enabled in this environment; contact PlayerPal |
| `401` | [`machine_auth_required`](./idempotency-and-errors.mdx#machine_auth_required) | Integration key missing or wrong |
| `403` | [`partner_scope_missing`](./idempotency-and-errors.mdx#partner_scope_missing) | Credential lacks the route's scope |

## Related

  - [Quickstart](./quickstart.mdx): 
    Sign a request and send your first batch.
  
  - [Idempotency and errors](./idempotency-and-errors.mdx): 
    Safe retries and every error code by endpoint family.
  
  - [Security](../trust/security.mdx): 
    How PlayerPal protects partner and player data.
