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
| Field | Type | Description |
|---|---|---|
code | String | Machine-readable error code. Use this for programmatic error handling. |
correlationId | String | Unique request identifier. Include this when contacting support. |
retryable | Boolean | true if the request can be safely retried after a short delay. |
category | String | Error 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.
| Code | Message | Resolution |
|---|---|---|
PAGE_SIZE_EXCEEDED | The 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_EXCEEDED | The 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_RANGE | The createdDateFrom is after createdDateTo. | Correct the date range so that createdDateFrom is before or equal to createdDateTo. |
QUERY_DEPTH_EXCEEDED | The query nesting depth exceeds the maximum of 10 levels. | Simplify your GraphQL query to reduce nesting depth. |
CURSOR_EXPIRED | The pagination cursor has expired (valid for 4 hours). | Start a new query from the first page by omitting the after argument. |
INVALID_CURSOR | The 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.
| Code | HTTP Status | Message | Resolution |
|---|---|---|---|
UNAUTHORIZED | 401 | Missing 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_MISSING | 200 | Credentials accepted but no merchant account resolved. | Contact support with your Shop ID. Your account may not be provisioned for API access. |
FORBIDDEN | 200 | You do not have access to the requested resource. | Verify your account permissions. Contact support with your Shop ID and the correlationId. |
Note:
UNAUTHORIZEDreturns a different response format (HTTP 401 with a plain JSON body, not the GraphQLerrorsenvelope):{ "error": "Unauthorized", "message": "Valid Basic Auth credentials required." }
PLATFORM errors
Infrastructure or concurrency issues. Most are retryable.
| Code | Retryable | Message | Resolution |
|---|---|---|---|
MERCHANT_CONCURRENCY_LIMIT_EXCEEDED | Yes | You have exceeded the per-merchant limit of 10 concurrent queries. | Wait for in-flight queries to complete before sending new requests. |
GLOBAL_CONCURRENCY_LIMIT_EXCEEDED | Yes | The system-wide concurrent query limit has been reached. | Retry after a short delay (2-10 seconds with exponential backoff). |
DATA_PLATFORM_ERROR | Yes | A transient error occurred while executing your query. | Retry after a short delay. Contact support if the error persists after 3 attempts. |
SERVICE_UNAVAILABLE | Yes | The service is temporarily unavailable. | Retry with exponential backoff (5-30 seconds). |
INTERNAL_ERROR | No | An 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.
| Code | Message | Resolution |
|---|---|---|
QUERY_TIMEOUT | The 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:
- Provide the correlation ID from the error response.
- Include the error code and message.
- Note the timestamp when the error occurred.
- 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
| Code | Category | HTTP | Retryable | Recommended retry delay |
|---|---|---|---|---|
PAGE_SIZE_EXCEEDED | VALIDATION | 200 | No | -- |
DATE_WINDOW_EXCEEDED | VALIDATION | 200 | No | -- |
INVALID_DATE_RANGE | VALIDATION | 200 | No | -- |
QUERY_DEPTH_EXCEEDED | VALIDATION | 200 | No | -- |
CURSOR_EXPIRED | VALIDATION | 200 | No | -- |
INVALID_CURSOR | VALIDATION | 200 | No | -- |
UNAUTHORIZED | SECURITY | 401 | No | -- |
MERCHANT_ID_MISSING | SECURITY | 200 | No | -- |
FORBIDDEN | SECURITY | 200 | No | -- |
MERCHANT_CONCURRENCY_LIMIT_EXCEEDED | PLATFORM | 200 | Yes | 1-5 seconds |
GLOBAL_CONCURRENCY_LIMIT_EXCEEDED | PLATFORM | 200 | Yes | 2-10 seconds |
DATA_PLATFORM_ERROR | PLATFORM | 200 | Yes | 1-5 seconds |
SERVICE_UNAVAILABLE | PLATFORM | 200 | Yes | 5-30 seconds |
INTERNAL_ERROR | PLATFORM | 200 | No | -- |
QUERY_TIMEOUT | TIMEOUT | 200 | Yes | Immediate (after simplifying query) |
Updated about 4 hours ago