Errors

The error envelope, status codes and failure semantics
View as Markdown

Error envelope

API errors return a standard JSON envelope with a human-readable detail and a stable, machine-readable code:

Error response
1{
2 "detail": "Insufficient balance",
3 "code": "insufficient_balance"
4}

Branch your error handling on code (stable), not detail (human-facing text that may change).

A few errors add machine-readable fields next to detail and code — for example a price_mismatch carries the real price:

Error response with extra fields
1{
2 "detail": "Price mismatch: the current price for this order is 26.50",
3 "code": "price_mismatch",
4 "actual_price": 26.50,
5 "submitted_price": 24.00
6}

Read those extras only for the code that documents them, and tolerate unknown keys — new ones can be added without a breaking change.

The token-exchange endpoint (POST /v1/auth/token/client) also emits a legacy error key alongside the envelope — e.g. {"error": "Invalid credentials", "detail": "Invalid credentials", "code": "invalid_credentials"}. Branch on code there too; error is only kept for older integrations.

detail is a plain human-readable fallback and never carries internal or infrastructure information. A 5xx therefore reports only that the service is unavailable — the underlying cause stays in our logs, not in your response body.

Status codes

HTTPcodeWhen
400validation_errorBad or missing field, unknown/unavailable item (including out of stock at create time), duplicate merchant_order_id, or out-of-range quantity.
400insufficient_balanceYour balance can’t cover the order.
400price_mismatchYou sent a price on POST /v1/orders and it isn’t what the order costs. Nothing was created or charged; the response carries actual_price. See Price protection.
401not_authenticatedNo access token was sent. Re-exchange your API key.
401invalid_tokenThe access token is malformed, expired, or not signed for this domain. Re-exchange your API key.
401token_revokedThe token was revoked (API key rotated, or the account disabled). Exchange the current key.
403api_access_not_allowedThe endpoint is dashboard-only — it has no API-channel contract.
403api_access_restrictedAPI access is switched off for this user; ask your account admin.
404not_foundOrder doesn’t exist, or isn’t owned by your account.
409already_existsConflicts with an existing resource.
429throttledRate limit exceeded. Back off and honour Retry-After.
502backend_unavailableUpstream failure. Retry with backoff; use a merchant_order_id so retries stay idempotent.
504timeoutThe request exceeded its deadline. Retry with backoff; same idempotency advice.

Examples

400 — validation error
1{ "detail": "Item is not available", "code": "validation_error" }
400 — insufficient balance
1{ "detail": "Insufficient balance", "code": "insufficient_balance" }
400 — price mismatch
1{
2 "detail": "Price mismatch: the current price for this order is 26.50",
3 "code": "price_mismatch",
4 "actual_price": 26.50,
5 "submitted_price": 24.00
6}
401 — not authenticated
1{ "detail": "Authentication credentials were not provided.", "code": "not_authenticated" }
404 — not found
1{ "detail": "Not found.", "code": "not_found" }

Create-time errors vs. async failures

There are two kinds of “failure”, and they surface in different places:

  • Create-time (HTTP) errors — the request is rejected and nothing is charged. These come back as a 4xx/5xx with the error envelope when you call POST /v1/orders. Example: an item out of stock at create time returns 400 validation_error (“Item is not available”).

  • Async failures (terminal status) — the order was accepted (201, status: "pending") and your balance was charged, but fulfillment later fails or only partially succeeds. This is not an HTTP error. The order settles on a terminal status of failed (with an error code — see Order error codes — and a full refund_amount) or partial (with a refund_amount for the undelivered units). You learn this from your webhook or GET /v1/orders/{id}.

Rule of thumb: a 201 means the order exists and was charged — watch its status, not the HTTP response, for the outcome. A 4xx means nothing happened; fix the request and retry.

Order error codes

When an order settles as failed (or partial), its error field carries a stable, machine-readable code and error_message carries a human-readable description. Branch on error (stable); show or log error_message (text that may change). Both appear on GET /v1/orders/{id} and in the webhook event.

Some of these conditions are usually caught up front at create time and returned as a 4xx instead (see Status codes); when they are only detected during fulfillment, they surface here as the order’s error.

errorerror_messageWhat it means
OUT_OF_STOCKItem is currently out of stockNo units were available to fulfill the order.
NOT_ENOUGH_STOCKNot enough stock for requested quantityFewer units were available than requested — typically a partial delivery, with the shortfall refunded.
ITEM_UNAVAILABLEItem is not availableThe item can’t be ordered right now (unpublished or disabled).
INVALID_REGIONInvalid region for this itemThe item isn’t available for the requested region.
ALREADY_OWNEDRecipient already owns this itemThe target account already owns this item.
INVALID_FIELDSInvalid or missing required fieldsA required entry in fields is missing or malformed.
INCORRECT_DETAILSIncorrect details providedOne or more submitted fields values were rejected.
INVALID_QUANTITYInvalid quantityquantity is out of range or invalid for this item.
STEAM_LOGIN_DOESNT_EXISTSteam login does not existThe steam_login value doesn’t match a real Steam account.
INCORRECT_USER_ACCOUNTIncorrect user account or player IDThe account or player id in fields (e.g. user_id) is wrong.
INSUFFICIENT_BALANCEInsufficient balanceYour balance couldn’t cover the order.
ORDER_FAILEDOrder processing failedFulfillment failed for an unspecified reason.
PROCESSING_FAILEDOrder processing failed, balance refundedFulfillment failed; the charge was refunded.
ORDER_TIMEOUTOrder was not fulfilled in time, balance refundedThe order wasn’t delivered within the fulfillment window; the charge was refunded.

On a failed order the charge is fully refunded (refund_amount equals the price). On a partial order only the undelivered units are refunded, and any delivered codes are still returned.

Retrying safely

Always send a unique merchant_order_id on POST /v1/orders. It is unique per account, so a retry (after a timeout or 5xx) that reuses the same value won’t place a duplicate order — a duplicate is rejected with a 400 validation_error.