# Errors

How the API signals failure: HTTP status codes on REST, an errors array on GraphQL, and RFC error bodies on OAuth.

Payer uses conventional HTTP status codes and returns a JSON body describing what went wrong. The exact shape depends on the surface you're calling.

## REST status codes

The Checkout API and other REST endpoints return these:

| Status | Meaning | Typical cause |
| --- | --- | --- |
| `200` | OK | The request succeeded. |
| `400` | Bad request | An action isn't valid for the resource's state — e.g. cancelling a paid session. |
| `401` | Unauthorized | Missing, malformed or unknown token. |
| `403` | Forbidden | The token lacks the required scope, or a PAT sent an `X-Active-Merchant` header. |
| `404` | Not found | No resource with that id for this account. |
| `409` | Conflict | An `Idempotency-Key` is still in flight, or was reused mid-request. |
| `422` | Unprocessable | A field failed validation, or an `Idempotency-Key` was reused with a different body. |
| `429` | Too many requests | The credential's per-minute [rate limit](/developers/docs/authentication#rate-limits) was exceeded. |

A simple error carries a `message`:

```json title="403 Forbidden"
{
  "message": "Forbidden",
  "error": "This credential does not have the required scope: checkout:write"
}
```

## Validation errors

Field validation failures return `422` with a Laravel-style `errors` map — one array of messages per offending field:

```json title="422 Unprocessable"
{
  "message": "The amount field is required. (and 1 more error)",
  "errors": {
    "amount": ["The amount field is required."],
    "reference": ["The reference has already been taken."]
  }
}
```

## GraphQL errors

GraphQL always responds `200` at the HTTP layer. Failures appear in a top-level `errors` array, and the affected field in `data` is `null`:

```json title="GraphQL error"
{
  "data": { "balance": null },
  "errors": [
    {
      "message": "This credential does not have the required scope: balances:read",
      "extensions": { "category": "authorization" }
    }
  ]
}
```

## OAuth errors

The [OAuth](/developers/docs/oauth) token and revoke endpoints follow RFC 6749 — a machine-readable `error` with a human `error_description`:

```json title="OAuth error"
{
  "error": "invalid_grant",
  "error_description": "The authorization code is invalid or has expired."
}
```

## Handling errors well

- **Retry writes with the same `Idempotency-Key`.** A `5xx` or a network blip is safe to retry — the original response is replayed, not repeated. See [Idempotency](/developers/docs/authentication#idempotency).
- **Back off on `429`.** Limits are per credential; spread load or split work across keys rather than hammering one.
- **Read `errors`, not just the field.** On GraphQL a partial response can carry both `data` and `errors`.
