Error reference
This page lists every kind of error Open Play returns, its status code, and its body format. When writing error handling code, first look at which endpoint family (core REST or plugin) is being called: the error bodies of the two families differ.
Warning
Most core REST endpoints return plain-text (
text/plain) errors, not JSON. Only the few errors marked “JSON” on this page carry a JSON envelope. Do not parse the error text; decide by the status code and (if present) the JSONcodefield. Plain-text messages can change between versions.
Error families
| Family | Which endpoints | Body |
|---|---|---|
| Core REST | /v1/characters…, /v1/wallets…, /v1/store…, /v1/auth/… etc. (endpoints) | Plain text; a few errors are JSON {code, message[, module]} |
| Plugin | /v1/plugins/… | Always JSON {code, message, retryable, requestId[, reason, module]} + the x-request-id header |
| Realtime | /v1/realtime/ws | Error frame: {type, requestId?, code, retryable} (protocol) |
| Chat socket | /v1/chat/ws | {"type":"error","code":"…"} (realtime overview) |
Core REST: status table
| Status | Body (plain text unless stated otherwise) | When | Retry? |
|---|---|---|---|
400 | A description of the error | Missing or broken header, invalid field, unknown command type, If-Match format | No; fix the request |
401 | unauthorized | API key or player session missing, invalid, expired; x-crx-env missing or broken; a banned account (no distinction is made) | No; renew the credentials |
403 | permission denied | The key type is insufficient (for example a server endpoint with a client key), CSRF or Origin rejection | No |
403 | JSON {"code":"account_banned","message":"account banned"} | On sign-in endpoints, a banned account after ownership has been proven | No; do not build a loop |
404 | not found | The resource does not exist or was deleted | No |
404 | JSON {"code":"module_disabled","message":"module disabled","module":"store"} | The product module is disabled in the environment | No; enable the module (modules) |
409 | version conflict: expected 3, actual 5 | The If-Match version does not match the real one | Re-read the state and repeat with the new version |
409 | already exists: <resource> | A resource with the same ID/key already exists | No |
409 | idempotent request in progress | The first request with the same Idempotency-Key is still being processed | Yes, shortly, with the same key |
410 | idempotent request result expired | The key’s record retention (24 hours) expired | With a new key, reading the state first |
413 | No body | The request body exceeded the 2 MiB limit (the image upload endpoint is subject to its own limit) | No |
422 | idempotency mismatch | Same key, different content | No; do not reuse the key |
422 | unprocessable: <reason> | A business rule rejection (insufficient balance, stock, limit…) | No; do not repeat without changing the state |
422 | rejected: <code> | A plugin business rule rejection (through core routes) | No |
429 | rate limited or rule budget exceeded: <rule> | Rate limit or rule budget | Yes, by waiting (rate limits); there is no Retry-After |
500 | internal error | An unexpected server error; no detail is leaked | Yes, bounded |
503 | tenant unavailable; header Retry-After: 30 | The environment’s placement is being moved or is temporarily unavailable | Yes, after Retry-After |
Notes:
- In body parsing errors, depending on the extractor,
415(wrongContent-Type) or422(JSON that does not conform to the schema) may also be seen. Rely only on the status code for the decision. - The
401and403difference: if the identity is missing or cannot be resolved,401; if the identity is valid but insufficient for this operation,403. On the player ticket endpoint all identity errors return403(realtime protocol). - If you call a server endpoint with a client key, you get
403 permission denied; if the key is unknown, it is401.
Purchase (POST /v1/player/store/listings/{listing_id}/buy) error codes
The player purchase endpoint states the rejection reason with a JSON code: {"code":"out_of_stock","message":"…"} (message is detail text; make your decision by code).
code | Status | Meaning |
|---|---|---|
not_visible | 404 | The listing does not exist or is not visible to this player |
listing_changed | 409 | The listing’s version (expected_version) changed; refresh the catalog and show it again |
out_of_stock | 422 | Out of stock |
limit_reached | 422 | The per-player purchase limit is full |
insufficient_funds | 422 | The balance is insufficient |
inventory_full | 422 | The inventory is full |
Other errors (400 header/body, 401, 403 Origin or CSRF, idempotency mismatch 422) are as in the general table above. An IAP listing cannot be bought from this endpoint (422). Details: store guide.
Plugin endpoints
Plugin endpoints return a JSON envelope; give the requestId value in support requests (the same value is also in the x-request-id header).
{"code":"unprocessable","message":"plugin command rejected","retryable":false,"requestId":"0192a3b4-0000-7000-8000-0000000000d1","reason":"street_reset_too_early"}
code | Status | retryable | Note |
|---|---|---|---|
unauthorized | 401 | no | Session/key missing or invalid; a client key |
forbidden | 403 | no | CSRF/Origin rejection; the plugin is not enabled; the version grant does not match; a banned account (message: account banned) |
module_disabled | 404 | no | The module field names the disabled module |
not_found | 404 | no | Unknown plugin or message type |
invalid_argument | 400 | no | Broken/missing header, unknown schema version, a payload that does not conform to the schema |
conflict | 409 | no | The If-Match version does not match |
idempotency_in_flight | 409 | yes | A request with the same key is being processed |
idempotency_mismatch | 422 | no | Same key, different body |
unprocessable | 422 | no | A business rule rejection; reason is a plugin-specific machine code |
idempotency_expired | 410 | no | The record expired |
rate_limited | 429 | yes | Retry-After: 60 |
tenant_unavailable | 503 | yes | Retry-After: 30 |
internal | 500 | yes |
Details and examples: calling plugins.
Note
On a plugin endpoint, a banned account, unlike the core identity endpoints, does not come with the
account_bannedcode:403,forbidden, andmessage: "account banned"is returned. You need this distinction only for a user-interface message; do not build a retry loop.
Differences between core and plugin
| Core REST | Plugin | |
|---|---|---|
| Body | Plain text (a few JSON exceptions) | Always JSON |
| Machine code | Only on errors that are JSON | code on every error |
retryable | None; decide by status code | Present |
| Retry delay | Only 503 (30 s) | 429 (60 s) and 503 (30 s) |
| Banned account | 403 account_banned (on sign-in endpoints) | 403 forbidden |
x-request-id | None | Present |
Error handling recommendation
- First look at the status code; for
401renew the session or have the player sign in again;403is an authorization or configuration problem (key type, CSRF, module, plugin state). - For
404, read the body: if it is JSON andmodule_disabled, the module is disabled; otherwise the resource does not exist. - Treat
409in two separate ways: a version conflict (re-read the state) versus idempotency “in progress” (wait and repeat with the same key). - Use exponential backoff on
429and503; if there is aRetry-After, wait for it (the example on the idempotency and concurrency page). 422is a business rule rejection; repeating the same request as is does not change the outcome.- The server does not leak error detail; when reporting a problem, give the status code, the request time, and (on plugin endpoints) the
requestId(troubleshooting).