# Errors

The error envelope every failed request returns, and what to do for each error code.

Source: https://getolet.com/resources/developers/errors/
Updated: 2026-10-15

Every failed request on the OLET API returns the same JSON shape: `{"error": {"code": "...", "message": "...", "fields": [...]}}`. `fields` only appears when the problem is about specific request fields. Branch your code on `code`, and show `message` to a person if you need to.

## What does the error envelope look like

```bash
curl https://api.oletapp.com/your-store/v1/products/does-not-exist \
  -H "Authorization: Bearer olet_live_YOUR_API_KEY"
```

```js
const res = await fetch(url, { headers })
if (!res.ok) {
  const { error } = await res.json()
  console.error(error.code, error.message)
}
```

A validation failure adds `fields`, a list of `{field, message}` pairs naming exactly what was wrong:

```json
{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "Check the highlighted fields and try again.",
    "fields": [
      {
        "field": "cursor",
        "message": "This cursor is invalid. Pass back the exact next_cursor value from a previous response, never one you construct yourself."
      }
    ]
  }
}
```

## What does each error code mean

Every code below is also listed in the `ErrorEnvelope` schema of the [OpenAPI spec](/resources/developers/build-with-ai/).

| Code | Status | What to do |
| --- | --- | --- |
| `UNAUTHORIZED` | 401 | Your credential is missing, malformed, unknown, revoked, or has gone idle past 90 days. Check the `Authorization` header and, for an idle credential, create a new one. |
| `VALIDATION_FAILED` | 400 | The request body, a query parameter (including `cursor`) or the `Idempotency-Key` header failed validation. Read `fields` and fix each one. A nested field is named with dots, such as `lines.2`. |
| `NOT_FOUND` | 404 | The resource does not exist, is deleted, or belongs to a different store. An id that is not a valid UUID, or a path that is not part of the API, answers the same way. Double-check the id. |
| `CONFLICT` | 409 | The write collided with an existing value or a rule, such as a SKU already in use, a duplicate brand or tag name, or deleting something that is still in use. Change the request and retry. |
| `UNPROCESSABLE` | 422 | The request referenced something that could not be found, exceeded a cap (for example more than 10 codes on one product), or otherwise could not be carried out as written. |
| `RATE_LIMITED` | 429 | You went over a rate limit, or too many product image imports were already running. See [rate limits](/resources/developers/rate-limits/) and slow down. |
| `IDEMPOTENCY_IN_PROGRESS` | 409 | A request with the same `Idempotency-Key` is still being processed. Wait and retry. |
| `IDEMPOTENCY_KEY_REUSED` | 422 | You reused an `Idempotency-Key` with a different request body. Use a new key per logical operation. |
| `INTERNAL` | 500 | Something went wrong on our side. Safe to retry after a short delay. |
| `INSUFFICIENT_SCOPE` | 403 | Your API key or OAuth connection lacks the scope this endpoint needs. See [authentication](/resources/developers/authentication/) for the scope table. |
| `PLAN_REQUIRED`, `PLAN_USERS_EXCEEDED`, `PRODUCT_LIMIT_REACHED` | 402 | Your store's current plan does not include API access (it needs the Growth plan), or a plan limit was reached, such as the product limit on a product create. Ask whoever manages billing to review the plan. |
| `RETAILER_CANCELLED`, `RETAILER_SUSPENDED`, `RETAILER_NOT_FOUND` | 403 | The store cannot currently take API calls because of its account status. Check the store's status in the back office. |
| `RETAILER_PAUSED`, `TRIAL_DECISION_REQUIRED` | 402 | The store's owner needs to act in Settings › Billing (resume the store, or choose a plan) before API calls resume. |
| `STALE_VERSION` | 409 | The `version` (or `expected_version`) you sent with a product update no longer matches the product. Reload it and try your change again. |
| `NOT_STOCK_TRACKED`, `REASON_DIRECTION_MISMATCH`, `INSUFFICIENT_STOCK` | 422 | An inventory rule refused the request, for example adjusting stock on a product that does not track it. Read `message` for the specific reason. |
| `REASON_NOT_ALLOWED` | 422 | The stock adjustment `reason_id` is a system reason or is turned off. Pick a reason that is enabled and is not a system reason. |
| `OUTLET_DELETED` | 422 | A stock write named an outlet that has been deleted. Nothing is written. |
| `IMAGE_FETCH_FAILED` | 422 | The `image_url` you supplied could not be downloaded. The product write itself succeeded, and the response carries the saved product in `data` next to the `error`. Check that the URL is public and reachable. |

Treat `message` as the source of truth for exactly what happened, and always check it alongside `code`.

## Do OAuth endpoints use the same envelope

No. `POST /oauth/token` answers in the standard OAuth shape, `{"error": "invalid_grant", "error_description": "..."}`, because OAuth clients expect it. See [authentication](/resources/developers/authentication/). Every other call returns the envelope above.

## Where to go next

See [rate limits](/resources/developers/rate-limits/) for what happens after too many requests, or the full [API reference](/resources/developers/reference/) for status codes on a specific endpoint.
