Skip to main content
AvailableVerified against 76c1f85Markdown

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.

You will
  • Send a batch of settled rounds
  • Check how the batch was processed
  • Backfill history with a bulk file upload
  • Retry safely without double counting
You need
  • The integration key PlayerPal issued to you
  • Your integration ID (pp_int_…)
  • Provider aliases (pp_plr_prv_…) from linking players
Time
About 20 minutes
For
Game providers

Batches or uploads?​

Activity batchActivity upload
EndpointPOST /v1/activity-batchesPOST /v1/activity-uploads, then PUT the file, then complete
FormatJSON bodyNDJSON, JSON, CSV, or any of those gzip-compressed
Best forFrequent small batches sent close to settlementLarge periodic exports, backfills
SizeOne request bodyUp 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 missing event_id, player_id or type, a player_id that is not a provider alias, or a malformed market. Nothing is stored. Fix the event and send the batch again.
  • Only round_settled is processed. Other types are accepted and then dropped.
  • Only EUR is processed. currency defaults to EUR; events in any other currency are dropped.
  • Events for unlinked aliases are dropped: unknown or revoked aliases, and rounds whose occurred_at is missing, before the Link's effective_from or at or after an Unlink.
  • Events without a positive bet, or with a market not 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.

Integration key and audience
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)
Environment
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

POST/v1/activity-batchesSubmit an activity event batchReference →

A batch is a batch_id and a list of events. Open events below to see every event field.

Request body
  • batch_idstringrequired
    Your id for the batch, unique per integration. Re-sending it with the same events returns the existing batch; different events are 409 idempotency_conflict.
  • eventsarray of objectrequired
  • schema_versionstring
    Defaults to 1.0.
  • event_id is your stable ID for the event, unique per integration. Do not reuse it in another batch.
  • player_id is the provider alias from linking, starting pp_plr_prv_.
  • occurred_at is the settlement time in ISO 8601 UTC. Without it the event cannot qualify.
  • bet and win are decimal strings with up to two decimal places, such as "2.50".
  • market is ISO 3166-1 alpha-2 in upper case, such as MT. If you omit it and your integration has exactly one market, that market is used.
Header parameters
  • x-integration-idstringrequired
    Integration public id; the batch audience comes only from this header.
  • idempotency-keystring
    Optional idempotency key; same key with a different payload is a conflict.
POST /v1/activity-batches
playerpal.sh
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, ...}
Response202
{
"batch_id": "rounds-2026-10-15-0001",
"state": "accepted",
"received_events": 1,
"received_at": "2026-10-15T12:04:00.120Z"
}
One event fails basic checks400
{
"statusCode": 400,
"message": "player_id_must_be_provider_alias",
"error": "Bad Request"
}

3Check processing

GET/v1/activity-batches/{batchId}Get activity batch statusReference →

202 means PlayerPal has stored the batch. Processing happens afterwards. Poll the batch with backoff to follow it.

stateMeaning
acceptedStored, waiting to be processed
processingEvents are being evaluated
processedEvery event was evaluated
processed_with_errorsProcessing 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.

Checkpoint

You get 200 with state processed and received_events matching the number of events you sent.

GET /v1/activity-batches/{batchId}
playerpal.sh
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, ...}
Response200
{
"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

POST/v1/activity-uploadsRequest a presigned upload for a bulk activity fileReference →

For large exports, upload a file instead of a JSON body.

  1. Request an upload with the file's content_type and exact content_length in bytes. You get an upload_id, a presigned upload_url, required_headers and an expires_at 15 minutes ahead by default.
  2. PUT the exact bytes to upload_url with every header in required_headers, without PlayerPal credentials. The file may not be larger than the size you declared.
  3. Complete the upload with a batch_id and the SHA-256 checksum of the file as 64 hex characters (a sha256: prefix is accepted). If you send row_count, it must equal the number of parsed events.
POST/v1/activity-uploads/{uploadId}/completeReference →

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.

Upload a file
playerpal.sh
# 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 ticket201
{
"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"
}
}
Complete202
{
"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_id is idempotent per integration. Re-sending the same batch_id with the same events returns the existing batch, with its current state. Event order does not matter.
  • Idempotency-Key is optional and works the same way.
  • The same batch_id or key with different events returns 409 idempotency_conflict. Use a new batch_id for 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 same batch_id.

See Idempotency and errors.

Same batch_id, different events409
{
"statusCode": 409,
"message": "idempotency_conflict",
"error": "Conflict"
}

Errors​

ResponseCauseFix
400 integration_id_requiredNo x-integration-id headerSend your integration ID
400 batch_id_required, events_requiredEmpty batch_id or no eventsSend at least one event under a batch_id
400 event_id_required, player_id_required, event_type_requiredAn event lacks a required fieldFix that event; the whole batch was rejected
400 player_id_must_be_provider_aliasA player_id does not start pp_plr_prv_Send the provider alias, never your own player ID
400 market_invalidmarket is not two upper-case lettersUse ISO 3166-1 alpha-2, such as MT
400 integration_not_foundUnknown integration IDUse the ID PlayerPal gave you
400 content_type_unsupported, content_length_required, object_too_largeUpload request not acceptedUse an allowed type and the exact size, within the limit
400 checksum_required, checksum_mismatchChecksum missing or not the uploaded bytesHash the exact file you uploaded
400 upload_expired, upload_incompleteThe slot expired or no file arrivedRequest a new upload and PUT the file before expires_at
400 row_count_mismatchrow_count differs from the parsed eventsFix the count or leave it out
400 object_not_json, gzip_invalid, format_unsupportedThe file could not be parsedSend NDJSON, JSON or CSV, optionally gzipped
401 machine_auth_requiredIntegration key missing or wrongSend it as x-api-key or Authorization: Bearer
404 upload_not_found, batch_not_foundUnknown ID for your integrationCheck the ID and x-integration-id
409 idempotency_conflictSame batch_id, key or upload, different contentUse a new batch_id for new data

The error code is in the message field.