# 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

```json
{
  "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"
      }
    }
  ]
}
```

| Field                  | Meaning                                                      |
| ---------------------- | ------------------------------------------------------------ |
| `message`              | A human-readable description of what went wrong.             |
| `extensions.code`      | A deterministic, stable error code you can branch on.        |
| `extensions.retryable` | `true` if the request may be safely retried without changes. |
| `extensions.detail`    | Optional extra context, such as which filter was invalid.    |
| `extensions.traceId`   | A 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**

```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

| Code                   | Retryable | What to do                                                      |
| ---------------------- | --------- | --------------------------------------------------------------- |
| `PAGE_SIZE_EXCEEDED`   | No        | Lower `first` to 5000 or fewer and page with `after`.           |
| `DATE_WINDOW_EXCEEDED` | No        | Narrow your date range (90 days max; 31 for movement balances). |
| `INVALID_DATE_RANGE`   | No        | Ensure the "from" date is not after the "to" date.              |
| `QUERY_DEPTH_EXCEEDED` | No        | Reduce query nesting to 10 levels or fewer.                     |

### Pagination cursors

| Code             | Retryable | What to do                                               |
| ---------------- | --------- | -------------------------------------------------------- |
| `CURSOR_EXPIRED` | No        | Cursors last 4 hours. Restart from the first page.       |
| `INVALID_CURSOR` | No        | Pass the exact `endCursor` value; do not modify cursors. |

### Authentication and access

| Code                  | Retryable | What to do                                                        |
| --------------------- | --------- | ----------------------------------------------------------------- |
| `UNAUTHORIZED`        | No        | Send valid Basic auth (Shop ID + API token). Also HTTP 401.       |
| `MERCHANT_ID_MISSING` | No        | Confirm your credentials belong to an active merchant account.    |
| `FORBIDDEN`           | No        | You requested data outside your account. Queries are auto-scoped. |

### Concurrency

| Code                                  | Retryable | What to do                                   |
| ------------------------------------- | --------- | -------------------------------------------- |
| `MERCHANT_CONCURRENCY_LIMIT_EXCEEDED` | Yes       | Reduce parallel requests (limit 10). Retry.  |
| `GLOBAL_CONCURRENCY_LIMIT_EXCEEDED`   | Yes       | Platform-wide; back off and retry.           |
| `TOO_MANY_REQUESTS`                   | Yes       | Slow your request rate; retry after a delay. |

### Timeouts and infrastructure

| Code                  | Retryable | What to do                                                     |
| --------------------- | --------- | -------------------------------------------------------------- |
| `QUERY_TIMEOUT`       | Yes       | Make the query more selective, then retry (15s limit).         |
| `DATABRICKS_ERROR`    | Yes       | Temporary data-platform problem. Back off and retry.           |
| `SERVICE_UNAVAILABLE` | Yes       | Service temporarily unavailable. Back off and retry.           |
| `INTERNAL_ERROR`      | No        | Not caused by your request. Contact support with the trace ID. |

### Validation

| Code               | Retryable | What to do                                                         |
| ------------------ | --------- | ------------------------------------------------------------------ |
| `INVALID_FILTER`   | No        | Check filter fields/values. Balance datasets cannot be aggregated. |
| `VALIDATION_ERROR` | No        | Review query arguments and filter values, then resubmit.           |
| `NOT_FOUND`        | No        | Verify the identifier is correct and within any date range.        |
| `CONFLICT`         | No        | Review 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

* [Troubleshooting](../stage-6/troubleshooting.md) — symptom-based debugging.

<br />