Idempotency, retries and errors
PlayerPal accepts at-least-once delivery and makes processing idempotent. You can resend any batch after a timeout, a dropped connection or a 5xx without creating duplicates, as long as you resend it unchanged under the same ID. This page is the reference for what each endpoint deduplicates on, how to retry, and every error code the partner API returns.
Watch first. This walks through the retry rules below in six short scenes.
Timeouts
A timeout, dropped connection or 5xx does not tell you whether PlayerPal received the batch. So you resend it.
Idempotency by endpoint family
Available| Endpoint family | Deduplicated on | Same ID, same content | Same ID, different content |
|---|---|---|---|
Fact ingest: POST /v1/ingest | Your party + batch_id | Original receipt, replayed: true | 409 idempotency_conflict |
Activity batches: POST /v1/activity-batches | Integration + batch_id, and optionally integration + idempotency-key header | The existing batch, with its current state | 409 idempotency_conflict |
Activity uploads: POST /v1/activity-uploads/{uploadId}/complete | The upload, its checksum and batch_id | The existing batch | 409 idempotency_conflict |
Linking: POST /v1/link-results/exchange | The one-time result code | Not repeatable: a second exchange returns 404 result_not_found | Not applicable |
Reads: GET routes | Not needed | Safe to repeat | Not applicable |
How "same content" is decided:
/v1/ingestcompares a hash of therecordsarray and theschema. Resend the identical array, in the same order./v1/activity-batchescompares a hash of the events, sorted byevent_id, so event order does not matter.- Events are also unique per integration and
event_id, and each Points award is unique per campaign rule, event and action. A replayed event never awards Points twice.
Accepted is not processed
A 202 means PlayerPal has durably accepted the batch. Points and analytics are computed afterwards, asynchronously. Do not wait on them before you mark the batch delivered in your outbox.
To correct fact data already sent to /v1/ingest, send the corrected facts in a new batch. Records upsert by their natural key and the stored revision is incremented.
Retry safely
- Give every batch a stable ID (
batch_id) when you create it, and store it with the batch. - On a timeout, connection error or
5xx, resend the identical batch with the same ID. - Back off exponentially with jitter between attempts, for example starting at a few seconds and capping at a minute or more. Keep retrying until the batch is accepted; do not drop it after a fixed number of attempts.
- If you are unsure whether a batch arrived, check it:
GET /v1/ingest/batches/{batchId}orGET /v1/activity-batches/{batchId}. Only poll after an ambiguous response. - Do not retry
400,401,403,404or409unchanged. Fix the request first.
| Response | Retry? |
|---|---|
2xx | No. Treat any 202 for your batch_id as delivered, including a replay |
400, 409 | No. Fix the request, or use a new ID for new data |
404 | No, except batch_not_found on a status check after an ambiguous send: the batch did not arrive, so resend it |
401, 403 | No. Fix the credential, clock, signer or scope |
500, 502, timeout, connection error | Yes, the identical request, with backoff |
503 | Not until PlayerPal confirms the feature is enabled for you |
A replayed activity batch reports its current state, which may already be past accepted.
Error format
Errors share one JSON body. The machine-readable code is in message. Branch on statusCode and message; the error field is the HTTP reason phrase.
{
"statusCode": 409,
"message": "idempotency_conflict",
"error": "Conflict"
}
Two responses do not carry a code. A body that is not valid JSON returns the JSON parser's message, and an unexpected server error returns the body below. Neither includes error. Retry a 500 with backoff.
{
"statusCode": 500,
"message": "Internal server error"
}
Error codes
Codes are grouped by the endpoint family that returns them. A code used by more than one family is described once, under the first family that uses it, and linked from the others.
Authentication (all partner routes)
Returned before the route runs. See Authentication for the signing rules.
partner_auth_required
401. The request has no Authorization: PP-HMAC-SHA256 … or Authorization: Bearer … header on a partner-credential route. Sign the request, or send an OAuth access token.
invalid_signature
401. The authorization header is malformed, the key ID is unknown, expired or revoked, or the signature does not match. Check your signer against the test vectors in Authentication, and sign the exact body bytes and query string you send.
stale_timestamp
401. X-PP-Timestamp is missing, is not Unix seconds, or is more than 300 seconds from PlayerPal's clock. Synchronise your clock with NTP and generate the timestamp just before sending.
invalid_nonce
401. X-PP-Nonce is missing or is not 16 to 128 characters of A-Z a-z 0-9 _ -. Generate a random nonce in that alphabet.
replayed_nonce
401. The nonce was already used with this key. Generate a fresh nonce for every request, including retries.
invalid_token
401. The bearer token failed signature, issuer, audience or expiry checks. Fetch a new token with the token endpoint and audience PlayerPal gave you.
unknown_client
401. The token is valid but its client is not registered to an active partner. Contact PlayerPal to register the client.
hmac_not_configured
401. Request signing is not enabled in this environment. Contact PlayerPal.
oauth_not_configured
401. OAuth tokens are not accepted in this environment. Use request signing, or contact PlayerPal.
partner_scope_missing
403. The credential is valid but lacks the route's scope: ingest.write, kpis.read or studio.read. Ask PlayerPal to add the scope.
machine_auth_required
401. On the activity and linking routes, the integration key is missing or wrong. Send it as x-api-key or Authorization: Bearer.
Fact ingest (/v1/ingest)
Envelope errors reject the whole call and store nothing. Record-level problems do not fail the call; see record rejections below. Also returned here: partner_scope_missing.
envelope_required
400. The body is not a JSON object. Send the batch envelope as a JSON object.
schema_unsupported
400. schema is missing or not a supported schema name. Use one of the schemas in Send fact batches.
schema_version_unsupported
400. schema_version is not 1. Send 1.
batch_id_invalid
400. batch_id is missing or is not 1 to 128 characters of A-Z a-z 0-9 _ . : -. Use a stable ID in that alphabet.
source_party_not_allowed_for_schema
400. source_party does not match the schema, for example OPERATOR on a game provider schema. Use the source party the schema requires.
source_id_required
400. source_id is missing or empty. Send your own party ID.
period_invalid
400. On ingest, period_start, period_end or generated_at is not a valid timestamp, or the period does not end after it starts. On KPI comparison routes, the period value does not match the grain. Fix the period.
records_required
400. records is missing or not an array. Send an array, which may be empty.
too_many_records
400. The batch has more than 10,000 records. Split it into several batches, each with its own batch_id.
source_id_must_be_authenticated_party
403. source_id is not the party your credential belongs to. Use the party ID PlayerPal gave you.
schema_not_allowed_for_party
403. The schema belongs to another party type, for example an operator sending a game provider schema. Send the schemas for your party type.
batch_not_found
404. No batch with that ID exists for your party (GET /v1/ingest/batches/{batchId}) or your integration (GET /v1/activity-batches/{batchId}, upload completion). Check the ID and the credential or x-integration-id you used.
idempotency_conflict
409. The ID was already used with different content: a batch_id with other records or another schema, an activity batch_id or idempotency-key with other events, or a completed upload with another checksum. Use a new ID for new data; resend the original unchanged for a retry.
Record rejections
A rejected record is listed in the receipt as { index, reason } and the rest of the batch is stored. Reasons include record_not_object, date_invalid, date_outside_period, player_ref_invalid, player_ref_unknown, game_ref_invalid, operator_ref_invalid, operator_ref_not_integrated, affiliate_ref_invalid, affiliate_ref_unknown, market_invalid, currency_required, currency_unsupported, activity_indicator_required, duplicate_in_batch and <field>_invalid for a malformed field. Each is explained in Send fact batches.
Activity (/v1/activity-batches, /v1/activity-uploads)
Unlike fact ingest, one invalid event rejects the whole activity batch. See Send activity. Also returned here: machine_auth_required, batch_not_found, idempotency_conflict and market_invalid.
integration_id_required
400. The x-integration-id header is missing. Send your integration ID on every activity call.
integration_not_found
400 on activity routes, 404 on linking and alias routes. The integration ID is unknown or not active. Check the x-integration-id value PlayerPal gave you.
batch_id_required
400. batch_id is missing or empty. Give every batch a stable ID.
events_required
400. The batch, or the uploaded file, has no events. Send at least one event.
event_id_required
400. An event has no event_id, or an uploaded line is not an object. Give every event a unique, stable ID.
player_id_required
400. An event has no player_id. Send the player's alias.
player_id_must_be_provider_alias
400. player_id is not a game provider alias (pp_plr_prv_…). Use the alias you received when the player linked.
event_type_required
400. An event has no type. Set the event type.
content_type_unsupported
400. The upload content type is not one of application/x-ndjson, application/json, application/gzip, application/x-gzip, text/csv or text/plain. Use a supported type.
content_length_required
400. The upload request has no positive whole-number content length. Send the file size in bytes.
object_too_large
400. The file is larger than the upload limit, or larger than the size you declared. Split it into several uploads.
object_storage_not_configured
400. Bulk uploads are not enabled in this environment. Send batches to /v1/activity-batches, or contact PlayerPal.
upload_not_found
404. No upload with that ID exists for your integration. Check the upload_id from the upload request.
upload_expired
400. The upload window closed before you completed it. Request a new upload.
checksum_required
400. The completion has no SHA-256 checksum of 64 hex characters. Send the checksum of the uploaded file.
checksum_mismatch
400. The uploaded file does not match the checksum you sent. Hash the exact bytes you uploaded.
upload_incomplete
400. PlayerPal found no file at the upload URL. Finish the upload before completing it.
object_empty
400. The uploaded file is empty. Upload a file with events.
format_unsupported
400. The file is in a format PlayerPal does not read, such as Parquet. Use JSON, NDJSON or CSV, optionally gzipped.
gzip_invalid
400. The file is declared as gzip but cannot be decompressed. Check the compression.
object_not_json
400. A line or document in the file is not valid JSON. Fix the file.
row_count_mismatch
400. The number of events in the file differs from the row count you declared. Check the file or the count.
Linking (/v1/link-intents, /v1/link-results/exchange, /v1/aliases/{playerAlias})
See Link players. Also returned here: machine_auth_required and integration_not_found.
return_origin_required
400. return_origin is missing. Send the origin PlayerPal returns the player to.
return_origin_invalid
400. return_origin is not a bare http or https origin: it has a path, query or fragment, or does not parse. Send only scheme, host and port.
operator_assertion_required
400. An operator-led link intent has no operator_assertion. Sign a short-lived assertion with your registered key.
assertion_invalid
400. The assertion's signature, audience or algorithm failed, or it lacks iss, sub or jti. Sign with your registered key using ES256 or RS256.
assertion_expired
400. The assertion has expired, or lives longer than five minutes. Sign a new assertion for each link intent.
assertion_replay
401. The assertion's jti was already used. Use a fresh jti for every assertion.
assertion_iss_mismatch
400. The assertion's iss is not your operator ID. Set iss to your operator ID.
assertion_forbidden_claims
400. The assertion contains personal data such as an email address, name or phone number. Remove those claims; PlayerPal never receives them.
jwks_unavailable
400. PlayerPal could not fetch your registered signing keys, or none are registered. Check that your key set URL is reachable and lists keys.
integration_not_authorized_for_operator
400. A requested game provider integration does not belong to your operator. Request only your own integrations.
scope_override_rejected
400. A game provider link intent also sent an operator header, an assertion or an audience in the body. With x-integration-id, send only return_origin.
party_not_found
404. The operator ID is unknown or is not an operator. Check the x-operator-id value.
result_code_required
400. The exchange has no result_code. Send the code your page received.
result_not_found
404. The result code is unknown, expired or already exchanged. Start a new link.
provider_alias_missing
400. The link has no alias for the integration in x-integration-id. Check that the link intent was created for that integration.
player_alias_required
400. The alias in the path is empty. Send the alias you hold.
audience_required
400. An alias status request has neither x-operator-id nor x-integration-id. Send the header for your audience.
alias_not_found
404. The alias was not issued to your audience. On alias status, treat the player as unlinked. On studio analytics, check the alias.
KPIs (/v1/kpis/*)
See KPI API. Also returned here: partner_scope_missing and period_invalid.
kpi_unknown
400. The kpi is not in the catalogue. Read GET /v1/kpis/catalogue for valid IDs.
kpi_not_available_for_party
400. The KPI exists but not for your party type. Use a KPI from your catalogue.
date_range_invalid
400. from or to is missing, not YYYY-MM-DD, or from is after to. Also returned by studio analytics. Fix the range.
date_range_too_long
400. The range covers 400 days or more. Shorten it.
grain_invalid
400. grain is not one of the values the route accepts (day, week or month; comparisons take week or month). Use a supported grain.
market_invalid
400. market is not a two-letter uppercase code. Also returned by activity events. Use a code such as MT.
market_not_supported
400. The KPI cannot be split by market. Leave out market.
breakdown_not_supported
400. The KPI has no breakdown. Choose a KPI that supports one.
dimension_invalid
400. The breakdown or explore dimension is not supported. Use one the route lists.
filter_invalid
400. An explore filter (game, market or partner) is malformed or unknown, or a required filter is missing. Use values from GET /v1/kpis/explore/options.
game_not_found
404. The game is not in your catalogue. Check the game reference.
insight_not_found
404. The insight ID is unknown. List insights again for current IDs.
month_range_invalid
400. A monthly range is not YYYY-MM, or starts after it ends. Fix the range.
month_range_too_long
400. A monthly range spans more than 24 months. Shorten it.
overview_not_available_for_party
400. Your party type has no overview. Contact PlayerPal.
period_not_closed
400. The comparison period has not ended yet. Compare a completed week or month.
kpi_backend_error
502. A temporary server-side problem. Retry with backoff.
kpi_backend_not_configured
503. KPIs are not enabled in this environment. Contact PlayerPal.
Studio analytics (/v1/analytics/studio*)
For game providers. Also returned here: partner_scope_missing, date_range_invalid and alias_not_found.
provider_party_required
403 when your credential is not a game provider's, 400 when no game provider party is identified. Use a game provider credential.
date_range_required
400. from or to is missing or not YYYY-MM-DD. Send both dates.
provider_not_found
404. The game provider is unknown. Contact PlayerPal.
studio_cohort_suppressed
404. Fewer accounts than the privacy threshold played in the range, so no figures are returned. Widen the range. See Privacy thresholds.
Rate limits
PlayerPal does not publish rate limits today. Send many records per batch rather than many small calls, and contact PlayerPal before a large backfill.