# Errors and status codes

HTTP status codes, REST error codes, MCP transport errors and tool errors.

Section: Commerce · Getting started
Canonical URL: https://docs.omniretailconnect.nl/commerce/getting-started/errors

## Overview

Both interfaces fail in two layers: a plain HTTP response when the request never reaches a capability (authentication, rate limit, size, routing), and a capability error when it does and fails (validation, scope, not found). Responses to routed requests of the MCP server and of the REST API carry `Cache-Control: no-store` (`no-store, private` on the API). Responses the framework answers itself, such as the 404 or 405 of a switched-off feature, or `OPTIONS`, `PUT` and `PATCH` on the MCP URL, carry `no-cache, private` instead.

## REST API

Every REST error is JSON with an `error` object. Messages are in English and safe to show; branch on `code`, not on the message. Errors never contain stack traces, SQL or paths.

**Error body**

```json
{
  "error": {
    "code": "invalid_argument",
    "message": "Unknown argument(s): foo."
  }
}
```

*REST API errors*

| Status | error.code | When |
| --- | --- | --- |
| `401` | `unauthenticated` | `Unauthorized.` Missing, malformed, unknown, revoked or expired token, or a token without the `api` channel. Identical for every cause. Has `WWW-Authenticate: Bearer`. |
| `403` | `forbidden_scope` | The token lacks the scope for the endpoint or option, for example `Searching orders by email requires the commerce.read.pii scope.` |
| `403` | `https_required` | `HTTPS is required.` Production only: the request is not secure. If you already use `https://`, ask the shop operator to check the reverse-proxy configuration. |
| `404` | `not_found` | The resource does not exist, the path is unknown (after authentication), a path value is malformed (`/products/abc`, a product id that is not digits only, or invalid percent-encoding or UTF-8 such as `/products/%FF`, for an authenticated caller), a parent category does not exist, or the API is switched off. |
| `405` | `method_not_allowed` | A method the existing path does not support, after authentication. `Allow` lists the methods of the path: `GET, HEAD` for reads, plus `POST`, `PUT`, `PATCH` or `DELETE` on the paths that have a write endpoint, for example `GET, HEAD, PUT, DELETE` on `/skus/{sku}/sale`. |
| `409` | `conflict` | A write conflicts with the current state: stock cannot be set while an external inventory provider manages it; an order is not an unpaid draft (confirm, cancel), a shipment is not allowed or an `idempotency_key` was used with other data; a discount code is taken or already used (delete); a page was saved after your `expected_updated_at`; or the data changed during the call or stayed locked by another change. |
| `414` | `uri_too_long` | The URL (path plus query string) is longer than the URL cap, 4096 bytes by default. |
| `422` | `invalid_argument` | Failed validation: for example `Unknown argument(s): foo.`, a bad or foreign cursor, conflicting or missing parameters, a range that is too long, or a parameter name or value that is not valid UTF-8. On a write also a body that is not a JSON object, not `application/json`, larger than 65536 bytes or that repeats a URL parameter. |
| `422` | `field_owned_by_pim` | A write touches a field that the PIM owns on a product linked to the PIM; the message names the field. |
| `429` | `too_many_requests` | `Too many requests.` A rate limit was exceeded. Has `Retry-After` (seconds). |
| `500` | `internal_error` | `An internal error occurred.` Unexpected failure. Details are logged on the server without personal data and never sent to the client. Safe to retry later. An authenticated caller also gets this for HTTP errors other than 404, 405, 414 and 429, for example `503` during maintenance. |

### The 401 response

Everything under `/api/v1` except `openapi.json` is behind authentication, including paths that do not exist and methods the path does not support. An unauthenticated caller therefore gets this identical response for every request: any HTTP method (including `TRACE`, `PURGE` or `PROPFIND`), any path, the bare prefix `/api/v1`, and paths with a malformed percent-encoding or invalid UTF-8 such as `/api/v1/products/%FF`. It learns nothing about which paths exist. Only `GET /api/v1/openapi.json` is public. Only an authenticated caller is told `404` (no such path, the bare prefix or a malformed path) or `405` (path exists, wrong method; `Allow` lists the methods of that path).

**401 · with WWW-Authenticate: Bearer**

```json
{
  "error": {
    "code": "unauthenticated",
    "message": "Unauthorized."
  }
}
```

### Order of checks

API enabled, HTTPS, URL length, per-IP limit, authentication, per-token limit, then the endpoint (scope, validation, lookup). Callers without a valid token therefore meet the `401` after the cheap edge checks, which do not depend on the path (`403` `https_required`, `414`) and the per-IP limit. Because the per-IP limit comes first, a flood of bad tokens from one address can also throttle a valid token from that same address (`429`). Other rules: the token of a request is refused from the exact instant it expires, and with several `Bearer` values in one `Authorization` header the last one is used (send exactly one).

## MCP: transport level

These are plain HTTP responses, in addition to normal JSON-RPC answers.

*MCP HTTP status codes*

| Status | Body | When |
| --- | --- | --- |
| `400` | JSON-RPC error | Unparsable JSON (`-32700`); a body that is not a JSON-RPC object (`-32600`); invalid `params` for `tools/list` or `tools/call` (`-32602`). |
| `401` | `{"message":"Unauthorized."}` | Missing, malformed, unknown, revoked or expired token, or a token without the `mcp` channel. All cases look identical. The response has the header `WWW-Authenticate: Bearer realm="mcp", error="invalid_token"`. |
| `403` | JSON-RPC error `-32600` | The request is not secure (production only): `HTTPS is required.` Use an `https://` URL. If you already use https, ask the shop operator to check the reverse-proxy configuration. |
| `404` | JSON-RPC error `-32601` | The JSON-RPC method is unknown (with a valid token). A switched-off server or a wrong path is not answered by the MCP endpoint: see "Switched-off features" below. The body `{"message":"Not Found."}` only appears when the server was switched off after the route list was cached. |
| `405` | — | `GET` or `DELETE` on the endpoint, with a valid token. The response has `Allow: POST`. Use `POST`. Without a valid token these requests get the `401` first. |
| `413` | JSON-RPC error `-32600` | The request body is larger than 256 KiB (64 KiB before server version 1.6.0). |
| `429` | `{"message":"Too Many Attempts."}` | A per-IP or per-token rate limit was exceeded. The response has `Retry-After` (seconds). Branch on the status code and `Retry-After`, not on the message text. |

The checks run in this order: server enabled, HTTPS, body size, per-IP limit, authentication, per-token limit, body shape. A `tools/call` for an unknown tool name answers a JSON-RPC `-32602` error.

See [Rate limits, pagination and request limits](https://docs.omniretailconnect.nl/commerce/getting-started/limits.md) for the numbers behind `413`, `414` and `429`.

## Switched-off features

While a feature is switched off, `GET` answers the shop's normal `404` and `POST`, `PUT`, `PATCH` and `DELETE` answer `405` (`Allow: GET, HEAD`), as for any unknown path. It is the shop's own 404 page, with a JSON body `{"message":""}` for API clients and an HTML page for browsers. A `POST` that answers `405` with `Allow: GET, HEAD` (message "The POST method is not supported for route mcp/commerce. Supported methods: GET, HEAD.") therefore means the URL is wrong or the MCP server is not enabled for this shop. The REST API behaves the same way. Only when the route list was cached while a feature was enabled and it was switched off later, the REST API answers the envelope `{"error":{"code":"not_found","message":"Not found."}}` with `404` for every method, and the MCP endpoint answers `{"message":"Not Found."}` with `404` for `GET`, `POST` and `DELETE`.

## MCP: tool errors

A failed tool call is still a normal `tools/call` result, with `isError: true`. The text content is JSON with a `code` and a `message` that is safe to show to the client.

**Text content of a failed tool call**

```json
{
  "code": "invalid_argument",
  "message": "The cursor is invalid. Start again without a cursor."
}
```

*Tool error codes*

| Code | Meaning |
| --- | --- |
| `invalid_argument` | Failed validation: an unknown argument, a bad cursor, conflicting or missing identifiers, or a range that is too long. REST: `422`. |
| `not_found` | The product, SKU, image, order, category or parent category does not exist. REST: `404`. |
| `forbidden_scope` | The token lacks the scope for the tool or option, for example an order search by email without `commerce.read.pii`. REST: `403`. Write tools are not shown to a token without the write scope; calling one answers JSON-RPC `-32602`, as for an unknown tool. |
| `field_owned_by_pim` | Write tools: the field, image or category placement belongs to the PIM on a product linked to the PIM, so the next import would overwrite it. Change it in the PIM. REST: `422`. |
| `conflict` | Write tools: `set_stock` while an external inventory provider manages stock, `create_product` in a shop that gets its products from the PIM, or the data changed during the call or stayed locked by another change. Read again and retry. REST: `409`. |
| `too_many_requests` | Write tools: the token made more than 30 write calls this minute (`Too many write requests. Try again in a minute.`). The call did not run; there is no `Retry-After`. Wait a minute. REST: `429`. |
| `internal_error` | An unexpected failure. The message is always `An internal error occurred.` Details are logged on the server without personal data. REST: `500`. |

The write tools and their errors are described in the [tool reference](https://docs.omniretailconnect.nl/commerce/mcp/tools.md#write-errors).

> **Note — Unknown SKUs are not errors:** `check_sku_availability` and `GET /availability` report an unknown SKU as `found: false`, and a variant whose inventory source cannot be reached as status `unavailable`. Neither is an error.
