Error Handling

The Merchant Data API returns errors using the standard GraphQL error format. This guide explains the error envelope, error categories, and how to build a resilient integration with retry logic.

The Merchant Data API returns errors in the standard GraphQL errors array. Each error
carries a human-readable message and an extensions object with a deterministic,
stable code and a retryable boolean.

Error shape

{
  "errors": [
    {
      "message": "The date range you requested is wider than the maximum allowed window.",
      "extensions": {
        "code": "DATE_WINDOW_EXCEEDED",
        "retryable": false,
        "traceId": "00-3f9a...-01"
      }
    }
  ]
}
FieldMeaning
messageA human-readable description of what went wrong.
extensions.codeA deterministic, stable error code you can branch on.
extensions.retryabletrue if the request may be safely retried without changes.
extensions.detailOptional extra context, such as which filter was invalid.
extensions.traceIdA trace identifier to quote when contacting support.

HTTP status codes

Most errors are returned with HTTP 200, in the GraphQL errors array. Two cases
differ:

  • HTTP 401 — credential failures (UNAUTHORIZED).
  • HTTP 400 — a request body that cannot be parsed as a valid GraphQL request.

Always inspect the errors array even on an HTTP 200 response.

Handling errors in code

Branch on extensions.code, and retry only when extensions.retryable is true.

Python

import time
import requests

RETRYABLE_MAX = 3

def run_query(query, variables=None):
    for attempt in range(RETRYABLE_MAX):
        resp = requests.post(
            "https://api.payretailers.com/data-api/graphql",
            auth=("YOUR_SHOP_ID", "YOUR_API_TOKEN"),
            headers={"Content-Type": "application/json"},
            json={"query": query, "variables": variables or {}},
            timeout=30,
        )
        body = resp.json()
        errors = body.get("errors")
        if not errors:
            return body["data"]

        err = errors[0]
        code = err["extensions"]["code"]
        retryable = err["extensions"].get("retryable", False)
        if retryable and attempt < RETRYABLE_MAX - 1:
            time.sleep(2 ** attempt)   # exponential backoff
            continue
        raise RuntimeError(f"{code}: {err['message']}")
    raise RuntimeError("Exhausted retries")

Retryable vs non-retryable

Retryable errors are transient. Wait briefly (ideally with exponential backoff) and
retry the same request:

  • MERCHANT_CONCURRENCY_LIMIT_EXCEEDED
  • GLOBAL_CONCURRENCY_LIMIT_EXCEEDED
  • QUERY_TIMEOUT
  • SERVICE_UNAVAILABLE
  • DATABRICKS_ERROR
  • TOO_MANY_REQUESTS

Non-retryable errors require a change to your request or credentials. Retrying
without changes will fail again.

Error codes

Governance

CodeRetryableWhat to do
PAGE_SIZE_EXCEEDEDNoLower first to 5000 or fewer and page with after.
DATE_WINDOW_EXCEEDEDNoNarrow your date range (90 days max; 31 for movement balances).
INVALID_DATE_RANGENoEnsure the "from" date is not after the "to" date.
QUERY_DEPTH_EXCEEDEDNoReduce query nesting to 10 levels or fewer.

Pagination cursors

CodeRetryableWhat to do
CURSOR_EXPIREDNoCursors last 4 hours. Restart from the first page.
INVALID_CURSORNoPass the exact endCursor value; do not modify cursors.

Authentication and access

CodeRetryableWhat to do
UNAUTHORIZEDNoSend valid Basic auth (Shop ID + API token). Also HTTP 401.
MERCHANT_ID_MISSINGNoConfirm your credentials belong to an active merchant account.
FORBIDDENNoYou requested data outside your account. Queries are auto-scoped.

Concurrency

CodeRetryableWhat to do
MERCHANT_CONCURRENCY_LIMIT_EXCEEDEDYesReduce parallel requests (limit 10). Retry.
GLOBAL_CONCURRENCY_LIMIT_EXCEEDEDYesPlatform-wide; back off and retry.
TOO_MANY_REQUESTSYesSlow your request rate; retry after a delay.

Timeouts and infrastructure

CodeRetryableWhat to do
QUERY_TIMEOUTYesMake the query more selective, then retry (15s limit).
DATABRICKS_ERRORYesTemporary data-platform problem. Back off and retry.
SERVICE_UNAVAILABLEYesService temporarily unavailable. Back off and retry.
INTERNAL_ERRORNoNot caused by your request. Contact support with the trace ID.

Validation

CodeRetryableWhat to do
INVALID_FILTERNoCheck filter fields/values. Balance datasets cannot be aggregated.
VALIDATION_ERRORNoReview query arguments and filter values, then resubmit.
NOT_FOUNDNoVerify the identifier is correct and within any date range.
CONFLICTNoReview request parameters. If unexpected, contact support.

Getting support

When you contact support about an error, quote the extensions.traceId value. It lets
the team correlate the request on their side.

_review_needed (from Stage 3): Error messages here are documentation-friendly
paraphrases written from the merchant's perspective. The exact wire text of each
message is not fixed in the source and may differ. Confirm final message wording with
the team before publication.

Next steps



Did this page help you?