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.



GraphQL error envelope

Most API errors return HTTP 200 with an errors array in the JSON response body. This is standard GraphQL behaviour -- the HTTP status code indicates the transport succeeded, and application-level errors are in the response payload.

The only exception is UNAUTHORIZED, which returns HTTP 401.

Error response structure

{
  "errors": [
    {
      "message": "Human-readable error description.",
      "extensions": {
        "code": "ERROR_CODE",
        "correlationId": "20260131T235915-a1b2c3d4",
        "retryable": false,
        "category": "VALIDATION"
      }
    }
  ]
}

Extensions fields

FieldTypeDescription
codeStringMachine-readable error code. Use this for programmatic error handling.
correlationIdStringUnique request identifier. Include this when contacting support.
retryableBooleantrue if the request can be safely retried after a short delay.
categoryStringError classification: VALIDATION, SECURITY, PLATFORM, or TIMEOUT.

Error categories

VALIDATION errors

Client input violates query governance rules. These are not retryable -- you must correct the request.

CodeMessageResolution
PAGE_SIZE_EXCEEDEDThe first parameter exceeds the maximum page size of 5000.Reduce the first argument to 5000 or fewer. Use pagination to retrieve more records.
DATE_WINDOW_EXCEEDEDThe date range exceeds the maximum window of 90 days.Narrow your date range to 90 days or fewer. Issue multiple queries with consecutive date windows for longer periods.
INVALID_DATE_RANGEThe createdDateFrom is after createdDateTo.Correct the date range so that createdDateFrom is before or equal to createdDateTo.
QUERY_DEPTH_EXCEEDEDThe query nesting depth exceeds the maximum of 10 levels.Simplify your GraphQL query to reduce nesting depth.
CURSOR_EXPIREDThe pagination cursor has expired (valid for 4 hours).Start a new query from the first page by omitting the after argument.
INVALID_CURSORThe pagination cursor format is invalid.Pass the exact endCursor value from a previous response. Do not modify cursor values manually.

SECURITY errors

Authentication or authorization failures. Not retryable unless you correct your credentials.

CodeHTTP StatusMessageResolution
UNAUTHORIZED401Missing or invalid credentials.Verify your Shop ID and API token. Ensure the Authorization header is Basic base64(YOUR_SHOP_ID:YOUR_API_TOKEN).
MERCHANT_ID_MISSING200Credentials accepted but no merchant account resolved.Contact support with your Shop ID. Your account may not be provisioned for API access.
FORBIDDEN200You do not have access to the requested resource.Verify your account permissions. Contact support with your Shop ID and the correlationId.

Note: UNAUTHORIZED returns a different response format (HTTP 401 with a plain JSON body, not the GraphQL errors envelope):

{
  "error": "Unauthorized",
  "message": "Valid Basic Auth credentials required."
}

PLATFORM errors

Infrastructure or concurrency issues. Most are retryable.

CodeRetryableMessageResolution
MERCHANT_CONCURRENCY_LIMIT_EXCEEDEDYesYou have exceeded the per-merchant limit of 10 concurrent queries.Wait for in-flight queries to complete before sending new requests.
GLOBAL_CONCURRENCY_LIMIT_EXCEEDEDYesThe system-wide concurrent query limit has been reached.Retry after a short delay (2-10 seconds with exponential backoff).
DATA_PLATFORM_ERRORYesA transient error occurred while executing your query.Retry after a short delay. Contact support if the error persists after 3 attempts.
SERVICE_UNAVAILABLEYesThe service is temporarily unavailable.Retry with exponential backoff (5-30 seconds).
INTERNAL_ERRORNoAn unexpected internal error occurred.Do not retry. Contact support with the correlationId.

TIMEOUT errors

Query execution exceeded the time limit. Retryable after simplifying the query.

CodeMessageResolution
QUERY_TIMEOUTThe query exceeded the 15-second execution timeout.Narrow the date range, reduce the page size, or remove unnecessary fields. Then retry.

Retry strategy

For retryable errors, use exponential backoff with jitter to avoid overwhelming the API.

Python

import time
import random
import requests
from requests.auth import HTTPBasicAuth

def execute_query_with_retry(url, query, max_retries=3):
    auth = HTTPBasicAuth("YOUR_SHOP_ID", "YOUR_API_TOKEN")
    headers = {"Content-Type": "application/json"}

    for attempt in range(max_retries + 1):
        response = requests.post(
            url,
            json={"query": query},
            auth=auth,
            headers=headers,
        )

        # Handle HTTP 401 (UNAUTHORIZED)
        if response.status_code == 401:
            raise Exception("Authentication failed. Check your Shop ID and API token.")

        data = response.json()

        # Check for GraphQL errors
        if "errors" in data:
            error = data["errors"][0]
            extensions = error.get("extensions", {})
            code = extensions.get("code", "UNKNOWN")
            retryable = extensions.get("retryable", False)

            if retryable and attempt < max_retries:
                base_delay = 2 ** attempt  # 1, 2, 4 seconds
                jitter = random.uniform(0, base_delay * 0.5)
                delay = base_delay + jitter
                print(f"Retryable error ({code}). Retrying in {delay:.1f}s "
                      f"(attempt {attempt + 1}/{max_retries})...")
                time.sleep(delay)
                continue
            else:
                correlation_id = extensions.get("correlationId", "N/A")
                raise Exception(
                    f"API error: {code} - {error['message']} "
                    f"(correlationId: {correlation_id})"
                )

        return data

# Usage
result = execute_query_with_retry(
    "https://api.payretailers.com/data-api/graphql",
    '{ payins(first: 10, filter: { createdDateFrom: "2026-01-01", createdDateTo: "2026-01-31" }) { edges { node { transactionId amount } } } }'
)

TypeScript

import axios, { AxiosError } from "axios";

interface GraphQLError {
  message: string;
  extensions?: {
    code: string;
    correlationId: string;
    retryable: boolean;
    category: string;
  };
}

async function executeQueryWithRetry(
  url: string,
  query: string,
  maxRetries = 3
): Promise<any> {
  const credentials = Buffer.from("YOUR_SHOP_ID:YOUR_API_TOKEN").toString("base64");

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const response = await axios.post(
        url,
        { query },
        {
          headers: {
            "Content-Type": "application/json",
            Authorization: `Basic ${credentials}`,
          },
        }
      );

      const data = response.data;

      if (data.errors && data.errors.length > 0) {
        const error: GraphQLError = data.errors[0];
        const retryable = error.extensions?.retryable ?? false;
        const code = error.extensions?.code ?? "UNKNOWN";

        if (retryable && attempt < maxRetries) {
          const baseDelay = Math.pow(2, attempt) * 1000;
          const jitter = Math.random() * baseDelay * 0.5;
          const delay = baseDelay + jitter;
          console.log(
            `Retryable error (${code}). Retrying in ${(delay / 1000).toFixed(1)}s...`
          );
          await new Promise((resolve) => setTimeout(resolve, delay));
          continue;
        }

        throw new Error(
          `API error: ${code} - ${error.message} ` +
            `(correlationId: ${error.extensions?.correlationId ?? "N/A"})`
        );
      }

      return data;
    } catch (err) {
      if (axios.isAxiosError(err) && err.response?.status === 401) {
        throw new Error("Authentication failed. Check your Shop ID and API token.");
      }
      throw err;
    }
  }
}

// Usage
const result = await executeQueryWithRetry(
  "https://api.payretailers.com/data-api/graphql",
  `{ payins(first: 10, filter: { createdDateFrom: "2026-01-01", createdDateTo: "2026-01-31" }) { edges { node { transactionId amount } } } }`
);

Using the correlation ID for debugging

Every error response includes a correlationId in the extensions. When contacting support:

  1. Provide the correlation ID from the error response.
  2. Include the error code and message.
  3. Note the timestamp when the error occurred.
  4. Describe the query you were executing (without credentials).

You can also set your own correlation ID by including the X-Correlation-ID header in your request. This makes it easier to trace requests in your own logs.

Full error reference

CodeCategoryHTTPRetryableRecommended retry delay
PAGE_SIZE_EXCEEDEDVALIDATION200No--
DATE_WINDOW_EXCEEDEDVALIDATION200No--
INVALID_DATE_RANGEVALIDATION200No--
QUERY_DEPTH_EXCEEDEDVALIDATION200No--
CURSOR_EXPIREDVALIDATION200No--
INVALID_CURSORVALIDATION200No--
UNAUTHORIZEDSECURITY401No--
MERCHANT_ID_MISSINGSECURITY200No--
FORBIDDENSECURITY200No--
MERCHANT_CONCURRENCY_LIMIT_EXCEEDEDPLATFORM200Yes1-5 seconds
GLOBAL_CONCURRENCY_LIMIT_EXCEEDEDPLATFORM200Yes2-10 seconds
DATA_PLATFORM_ERRORPLATFORM200Yes1-5 seconds
SERVICE_UNAVAILABLEPLATFORM200Yes5-30 seconds
INTERNAL_ERRORPLATFORM200No--
QUERY_TIMEOUTTIMEOUT200YesImmediate (after simplifying query)