# Quickstart

Make one complete round trip with the PlayerPal Partner API: sign a request, send a batch of daily facts, confirm the receipt and read a KPI back. Everything here runs outside game launch, wagering and settlement, so nothing you build in this guide can block play.

:::tip[Using OAuth instead?]
If PlayerPal registered an OAuth client for you, send `Authorization: Bearer <access-token>` and skip step 2. Everything else is the same. See [Authentication](./authentication.mdx).
:::

## Set up your credentials

PlayerPal issues credentials during [onboarding](./onboarding.mdx). For request signing you receive a key ID (`pp_kid_…`) and a secret (`pp_sk_…`).

- The secret is shown **once**. Store it in your secret manager, never in source code.
- Ask for both scopes you need. A key issued without named scopes can only read KPIs (`kpis.read`); sending facts needs `ingest.write`.
- You can hold two active keys at a time. After a rotation, the old key keeps working for 24 hours.

The samples in this guide read three environment variables.

Environment:

```
export PP_API_BASE="https://<playerpal-api-host>"
export PP_KEY_ID="pp_kid_XXXX"
export PP_SECRET="<your-secret>"
```

## Sign every request

Each request carries three headers: `Authorization`, `X-PP-Timestamp` and `X-PP-Nonce`. The signature is an HMAC-SHA256 of a **canonical string**: six lines joined with `\n`, no trailing newline.

1. HTTP method, uppercase
2. Path, without the query string
3. Query pairs sorted by key then value, RFC 3986 encoded, joined with `&` (empty if none)
4. Lowercase hex SHA-256 of the exact body bytes (of the empty string for `GET`)
5. The timestamp, in Unix seconds
6. The nonce, 16 to 128 characters of `A-Z a-z 0-9 _ -`, never reused

The signature is the result, base64url-encoded without padding. The timestamp must be within 300 seconds of PlayerPal's clock.

With secret `pp_sk_test-vector-secret`, timestamp `1760000000` and nonce `nonce_0123456789abcdef`, signing `GET /v1/kpis/own?to=2026-09-30&kpi=own.rounds&from=2026-09-01` gives `NhQFk3mISFyIhDIMHBwZ-pWBoO5pKuFk_yWDVycaOyY`. Every sample on this page is tested against this value.

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

## Send your first batch

`ingestSubmit` (see the API reference)

A batch is one envelope with records of one schema. This example sends one player's daily gameplay as a game provider. Operators send operator schemas the same way; see [Fact ingest](./fact-ingest.mdx) for every schema.

- `source_id` must be your own party ID.
- `player_ref` must be an alias PlayerPal issued to you. Anything else, such as an email address, is rejected.
- Sign the exact bytes you send. Re-serialising the JSON after signing breaks the signature.

(Parameter table: see the API reference.)

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

Response:

```
{
  "batch_id": "daily-2026-10-12-0001",
  "schema": "playerpal.provider.player_game_daily.v1",
  "received_at": "2026-10-13T01:15:02.000Z",
  "records_received": 1,
  "records_accepted": 1,
  "records_rejected": 0,
  "rejections": [],
  "replayed": false
}
```

## Read the receipt

PlayerPal answers `202 Accepted` with a receipt. Records are validated one by one: a bad record is listed in `rejections` as `{ index, reason }` and never fails the rest of the batch.

The `batch_id` is your idempotency key. If a call times out, send the **identical** batch again: you get the original receipt back with `replayed: true`, and nothing is stored twice. Sending different records under the same `batch_id` returns `409 idempotency_conflict`.

(Parameter table: see the API reference.)

`records_accepted` is `1`. Send the same batch again and the receipt comes back with `replayed: true`.

`ingestReceipt` (see the API reference)

Same batch_id, different records:

```
{
  "statusCode": 409,
  "message": "idempotency_conflict",
  "error": "Conflict"
}
```

A rejected record in the receipt:

```
{
  "records_received": 2,
  "records_accepted": 1,
  "records_rejected": 1,
  "rejections": [{ "index": 1, "reason": "player_ref_invalid" }],
  "replayed": false
}
```

## Read a KPI

`kpisOwn` (see the API reference)

Every `/v1/kpis` route needs the `kpis.read` scope and is signed the same way; a `GET` signs the hash of the empty body. Send the query string exactly as you signed it.

`own` returns one KPI from your [catalogue](./kpi-api.mdx) as a series, with the total, the same-length previous period and the change between them. A period backed by fewer than 50 distinct accounts comes back `suppressed` instead of a value. See [Privacy thresholds](../analytics/privacy-thresholds.mdx).

You get `200` with a `series` covering every week in the range.

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

Response:

```
{
  "kpi": "own.rounds", "label": "Rounds", "unit": "count",
  "from": "2026-09-01", "to": "2026-09-30", "grain": "week",
  "series": [
    { "period": "2026-08-31", "value": 1240 },
    { "period": "2026-09-07", "value": null, "suppressed": true }
  ],
  "total": { "value": 5120 },
  "previous": { "from": "2026-08-02", "to": "2026-08-31", "value": 4800 },
  "delta_pct": 0.0667,
  "generated_at": "2026-10-13T12:00:00.000Z"
}
```

## If something goes wrong

| Response | Cause | Fix |
|---|---|---|
| `401 invalid_signature` | The canonical string or secret differs from PlayerPal's | Check your signer against the test value in step 2; sign the exact body bytes |
| `401 stale_timestamp` | Your clock is more than 300 seconds off | Sync the clock with NTP |
| `401 replayed_nonce` | A nonce was reused | Generate a fresh random nonce per request |
| `403 partner_scope_missing` | The key lacks the route's scope | Ask PlayerPal to add `ingest.write` or `kpis.read` |
| `403 source_id_must_be_authenticated_party` | `source_id` is not your party ID | Use the party ID PlayerPal gave you |
| `409 idempotency_conflict` | Same `batch_id`, different records | Use a new `batch_id` for new data |

Every error code is listed in [Idempotency and errors](./idempotency-and-errors.mdx).

## Next steps

  - [Link players](./linking-players.mdx): 
    Connect casino accounts to PlayerPal and receive the aliases you send facts for.
  
  - [Send activity](./sending-activity.mdx): 
    Stream game rounds in batches or bulk uploads for points and analytics.
  
  - [API reference](/api/partner): 
    Every partner endpoint, with parameters and responses, generated from the API.
