Skip to main content
AvailableVerified against 76c1f85Markdown

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.

A timeout does not say whether the batch arrivedPartner outboxbatch_id: b-0001PlayerPalreceives batchessend 1no responsesend 2 (same)Resend the identical batch, under the same batch_id.
0:00 / 0:55

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 familyDeduplicated onSame ID, same contentSame ID, different content
Fact ingest: POST /v1/ingestYour party + batch_idOriginal receipt, replayed: true409 idempotency_conflict
Activity batches: POST /v1/activity-batchesIntegration + batch_id, and optionally integration + idempotency-key headerThe existing batch, with its current state409 idempotency_conflict
Activity uploads: POST /v1/activity-uploads/{uploadId}/completeThe upload, its checksum and batch_idThe existing batch409 idempotency_conflict
Linking: POST /v1/link-results/exchangeThe one-time result codeNot repeatable: a second exchange returns 404 result_not_foundNot applicable
Reads: GET routesNot neededSafe to repeatNot applicable

How "same content" is decided:

  • /v1/ingest compares a hash of the records array and the schema. Resend the identical array, in the same order.
  • /v1/activity-batches compares a hash of the events, sorted by event_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​

  1. Give every batch a stable ID (batch_id) when you create it, and store it with the batch.
  2. On a timeout, connection error or 5xx, resend the identical batch with the same ID.
  3. 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.
  4. If you are unsure whether a batch arrived, check it: GET /v1/ingest/batches/{batchId} or GET /v1/activity-batches/{batchId}. Only poll after an ambiguous response.
  5. Do not retry 400, 401, 403, 404 or 409 unchanged. Fix the request first.
ResponseRetry?
2xxNo. Treat any 202 for your batch_id as delivered, including a replay
400, 409No. Fix the request, or use a new ID for new data
404No, except batch_not_found on a status check after an ambiguous send: the batch did not arrive, so resend it
401, 403No. Fix the credential, clock, signer or scope
500, 502, timeout, connection errorYes, the identical request, with backoff
503Not 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.

Error body409
{
"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.

Unexpected server error500
{
"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.

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.