Send player activity
Game providers send PlayerPal the rounds that linked players settle. PlayerPal turns eligible rounds into Points and into the activity KPIs you can read from the KPI API. Activity is sent after settlement, from your backend, asynchronously. It never sits in the wager or settlement path, so a PlayerPal outage cannot block play.
Batches or uploads?
| Activity batch | Activity upload | |
|---|---|---|
| Endpoint | POST /v1/activity-batches | POST /v1/activity-uploads, then PUT the file, then complete |
| Format | JSON body | NDJSON, JSON, CSV, or any of those gzip-compressed |
| Best for | Frequent small batches sent close to settlement | Large periodic exports, backfills |
| Size | One request body | Up to the upload limit (64 MiB by default) |
Both paths end in the same batch, with the same validation, states and idempotency.
What PlayerPal does with your events today
Available- The whole batch is rejected (
400) if any event fails basic checks: a missingevent_id,player_idortype, aplayer_idthat is not a provider alias, or a malformedmarket. Nothing is stored. Fix the event and send the batch again. - Only
round_settledis processed. Other types are accepted and then dropped. - Only
EURis processed.currencydefaults toEUR; events in any other currency are dropped. - Events for unlinked aliases are dropped: unknown or revoked aliases, and rounds whose
occurred_atis missing, before the Link'seffective_fromor at or after an Unlink. - Events without a positive
bet, or with amarketnot configured for your integration, are also dropped.
Dropped events never fail the rest of the batch, and they earn nothing.
1Set up the integration key
Activity routes use the integration key PlayerPal issues during onboarding, sent as x-api-key: <your-integration-key> or Authorization: Bearer <your-integration-key>.
Every request also sends x-integration-id: pp_int_XXXX. The batch belongs to that integration only; operator_id in a body is accepted and ignored. Keep the key server-side.
# 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)
export PP_API_BASE="https://<playerpal-api-host>"
export PP_INTEGRATION_KEY="<your-integration-key>"
export PP_INTEGRATION_ID="pp_int_XXXX"
2Send a batch
A batch is a batch_id and a list of events. Open events below to see every event field.
batch_idstringrequiredYour id for the batch, unique per integration. Re-sending it with the same events returns the existing batch; different events are409 idempotency_conflict.eventsarray of objectrequiredschema_versionstringDefaults to1.0.
event_idis your stable ID for the event, unique per integration. Do not reuse it in another batch.player_idis the provider alias from linking, startingpp_plr_prv_.occurred_atis the settlement time in ISO 8601 UTC. Without it the event cannot qualify.betandwinare decimal strings with up to two decimal places, such as"2.50".marketis ISO 3166-1 alpha-2 in upper case, such asMT. If you omit it and your integration has exactly one market, that market is used.
x-integration-idstringrequiredIntegration public id; the batch audience comes only from this header.idempotency-keystringOptional idempotency key; same key with a different payload is a conflict.
curl -sS -X POST "$API/v1/activity-batches" \
-H "x-api-key: $INTEGRATION_KEY" -H "x-integration-id: $INTEGRATION_ID" \
-H 'Content-Type: application/json' \
-d '{
"batch_id": "rounds-2026-10-15-0001",
"events": [{
"event_id": "round-8812-settled", "type": "round_settled",
"player_id": "pp_plr_prv_XXXX", "occurred_at": "2026-10-15T12:03:11Z",
"game_ref": "your-game-ref", "round_ref": "round-8812",
"currency": "EUR", "bet": "2.00", "win": "0.00"
}]
}'
# 202 {batch_id, state, received_events, ...}
{
"batch_id": "rounds-2026-10-15-0001",
"state": "accepted",
"received_events": 1,
"received_at": "2026-10-15T12:04:00.120Z"
}
{
"statusCode": 400,
"message": "player_id_must_be_provider_alias",
"error": "Bad Request"
}
3Check processing
202 means PlayerPal has stored the batch. Processing happens afterwards. Poll the batch with backoff to follow it.
state | Meaning |
|---|---|
accepted | Stored, waiting to be processed |
processing | Events are being evaluated |
processed | Every event was evaluated |
processed_with_errors | Processing finished but some events are still pending |
completed_at stays null until processing finishes. Per-event outcomes, including dropped events, are not returned by the API today. Your acceptance rate is available as the own.ingest_acceptance KPI.
You get 200 with state processed and received_events matching the number of events you sent.
curl -sS "$API/v1/activity-batches/rounds-2026-10-15-0001" \
-H "x-api-key: $INTEGRATION_KEY" -H "x-integration-id: $INTEGRATION_ID"
# 200 {state, received_events, completed_at, ...}
{
"batch_id": "rounds-2026-10-15-0001",
"state": "processed",
"received_events": 1,
"received_at": "2026-10-15T12:04:00.120Z",
"completed_at": "2026-10-15T12:04:00.480Z"
}
4Bulk upload for backfills
For large exports, upload a file instead of a JSON body.
- Request an upload with the file's
content_typeand exactcontent_lengthin bytes. You get anupload_id, a presignedupload_url,required_headersand anexpires_at15 minutes ahead by default. PUTthe exact bytes toupload_urlwith every header inrequired_headers, without PlayerPal credentials. The file may not be larger than the size you declared.- Complete the upload with a
batch_idand the SHA-256checksumof the file as 64 hex characters (asha256:prefix is accepted). If you sendrow_count, it must equal the number of parsed events.
POST /v1/activity-uploads/{uploadId}:complete is the same call. The response is the same 202 as a batch, and the same all-or-nothing checks apply to the file's events.
File formats. NDJSON has one event object per line. JSON can be an array of events, an object with an events array, or a single event. CSV uses a header row with the field names; without a header, columns are read in this order: event_id, player_id, type, bet, win, currency, occurred_at, game_ref, round_ref, market. CSV values cannot contain commas. Gzip is detected from the content type or the file itself. Parquet is not supported.
# 1. Ask for an upload slot for the exact file size.
SIZE=$(wc -c < "$FILE" | tr -d ' ')
TICKET=$(curl -sS -X POST "$API/v1/activity-uploads" \
-H "x-api-key: $INTEGRATION_KEY" -H "x-integration-id: $INTEGRATION_ID" \
-H 'Content-Type: application/json' \
-d "{\"content_type\":\"application/x-ndjson\",\"content_length\":$SIZE}")
UPLOAD_ID=$(echo "$TICKET" | jq -r .upload_id)
UPLOAD_URL=$(echo "$TICKET" | jq -r .upload_url)
# 2. Upload the file as-is, with the headers from the ticket, before it expires.
HEADER_ARGS=(); while IFS= read -r h; do HEADER_ARGS+=(-H "$h"); done \
< <(echo "$TICKET" | jq -r '.required_headers | to_entries[] | "\(.key): \(.value)"')
curl -sS -X PUT "$UPLOAD_URL" "${HEADER_ARGS[@]}" --data-binary @"$FILE"
# 3. Complete: PlayerPal parses the file into an activity batch.
CHECKSUM=$(openssl dgst -sha256 -hex "$FILE" | awk '{print $NF}')
curl -sS -X POST "$API/v1/activity-uploads/$UPLOAD_ID/complete" \
-H "x-api-key: $INTEGRATION_KEY" -H "x-integration-id: $INTEGRATION_ID" \
-H 'Content-Type: application/json' \
-d "{\"batch_id\":\"backfill-2026-10-0001\",\"checksum\":\"$CHECKSUM\"}"
# 202 {batch_id, state, received_events, ...}
{
"upload_id": "upl_XXXX",
"upload_url": "<presigned-upload-url>",
"expires_at": "2026-10-15T12:19:00.000Z",
"required_headers": {
"Content-Type": "application/x-ndjson",
"Content-Length": "1843302"
}
}
{
"batch_id": "backfill-2026-10-0001",
"state": "accepted",
"received_events": 18210,
"received_at": "2026-10-15T12:10:42.000Z"
}
5Retry safely
Network errors happen. Retry on timeouts and 5xx with the same batch_id, and nothing is counted twice.
batch_idis idempotent per integration. Re-sending the samebatch_idwith the same events returns the existing batch, with its currentstate. Event order does not matter.Idempotency-Keyis optional and works the same way.- The same
batch_idor key with different events returns409 idempotency_conflict. Use a newbatch_idfor new data. - Completing an upload twice with the same checksum returns the original batch; a different checksum is
409 idempotency_conflict. - If a batch was rejected with
400, nothing was stored: fix it and send it again under the samebatch_id.
{
"statusCode": 409,
"message": "idempotency_conflict",
"error": "Conflict"
}
Errors
| Response | Cause | Fix |
|---|---|---|
400 integration_id_required | No x-integration-id header | Send your integration ID |
400 batch_id_required, events_required | Empty batch_id or no events | Send at least one event under a batch_id |
400 event_id_required, player_id_required, event_type_required | An event lacks a required field | Fix that event; the whole batch was rejected |
400 player_id_must_be_provider_alias | A player_id does not start pp_plr_prv_ | Send the provider alias, never your own player ID |
400 market_invalid | market is not two upper-case letters | Use ISO 3166-1 alpha-2, such as MT |
400 integration_not_found | Unknown integration ID | Use the ID PlayerPal gave you |
400 content_type_unsupported, content_length_required, object_too_large | Upload request not accepted | Use an allowed type and the exact size, within the limit |
400 checksum_required, checksum_mismatch | Checksum missing or not the uploaded bytes | Hash the exact file you uploaded |
400 upload_expired, upload_incomplete | The slot expired or no file arrived | Request a new upload and PUT the file before expires_at |
400 row_count_mismatch | row_count differs from the parsed events | Fix the count or leave it out |
400 object_not_json, gzip_invalid, format_unsupported | The file could not be parsed | Send NDJSON, JSON or CSV, optionally gzipped |
401 machine_auth_required | Integration key missing or wrong | Send it as x-api-key or Authorization: Bearer |
404 upload_not_found, batch_not_found | Unknown ID for your integration | Check the ID and x-integration-id |
409 idempotency_conflict | Same batch_id, key or upload, different content | Use a new batch_id for new data |
The error code is in the message field.