> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.morphiaone.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.morphiaone.ai/_mcp/server.

# Errors

Every failure from this API — every status, every endpoint — answers with the same
envelope:

```json
{
  "error": {
    "code": "INSUFFICIENT_CREDITS",
    "message": "This request needs 1 credit and the account has 0.",
    "request_id": "c7a4e2f0-5b6d-4a18-9e3c-2f1d0b8a7c64",
    "details": []
  }
}
```

The same `request_id` is also sent as the `X-Request-Id` response header.

## The contract is split down the middle

| Field           | Audience         | Stability                                                                     |
| --------------- | ---------------- | ----------------------------------------------------------------------------- |
| `error.code`    | **your code**    | stable. Renaming or removing one is a breaking change.                        |
| `error.message` | **a human**      | none. Wording changes without notice.                                         |
| `request_id`    | support          | present on every error. Log it.                                               |
| `details`       | both, cautiously | always an array of `{field, code, message}`; empty when nothing more is known |

That split is the whole design. A stable machine code means you never have to write
`if "insufficient" in message.lower()`, which is the pattern that breaks when
someone improves a sentence. So:

```python
if error["code"] == "INSUFFICIENT_CREDITS":   # yes: codes are the contract
    alert_billing()
if "credit" in error["message"]:              # no: messages change without notice
    alert_billing()
```

A `400 INVALID_REQUEST` carries one `details[]` entry per rejected field, and its
`details[].code` — `duplicate_product_category`, for instance — is as stable as
`error.code`. Branch on either; never on a `message`.

Adding a new code is a normal, non-breaking change, so treat an unrecognised code
as "a failure of this status class" rather than crashing on it.

## Statuses are never collapsed

An authentication failure, an exhausted quota, a rate limit and a dependency outage
each get their own status. None of them are answered as `400`.

```text
400  your request is malformed          do not retry unchanged
401  no usable key                      do not retry unchanged
402  out of credits                     do not retry until topped up
403  key usable, lacks the scope        do not retry unchanged
404  no such job or asset, or not yours do not retry
409  idempotency conflict               retry only IDEMPOTENCY_IN_PROGRESS
410  the input asset expired            upload again
413  upload too large                   do not retry unchanged
415  not a supported image type         do not retry unchanged
422  an image that cannot be used       do not retry unchanged
429  slow down                          retry after Retry-After
500  our bug                            report with the request_id
503  unavailable                        retry after Retry-After
```

The point of not collapsing them is that your retry logic is a function of the
status. If every failure were a `400`, a correct client would be impossible to
write: it could not distinguish "you are wrong" from "come back in a second".

Two pairs worth knowing apart:

* **`429 TENANT_QUEUE_LIMIT_EXCEEDED`** is your account's own queue being full.
  **`503 QUEUE_CAPACITY_EXCEEDED`** is ours. Both are retryable; only the first is
  something you can fix by submitting less.
* **`503 SERVICE_UNAVAILABLE`** during authentication is a `503` and not a `401`
  because your key was never checked. Do not discard a key over it.

## Work failures are not HTTP failures

A submission that returns `202` and then fails during analysis reports that failure
on a `200` poll, with the same envelope inside the job:

```json
{ "status": "failed", "error": { "code": "INPUT_UNPROCESSABLE", "message": "..." } }
```

See [Asynchronous jobs](/concepts/jobs). An HTTP error code is about your request;
`job.status` is about the work.

## What an error will never contain

By design, and enforced in our own tests: no image bytes, no facial landmarks, no
API key or fragment of one, no internal file path, no storage or cache key, no
stack trace, no upstream URL.

If you are wiring errors into your own logs and alerts, the safe fields are
`error.code`, `error.message`, `request_id`, the job id, the operation, the HTTP
status, and your own timing. That is also exactly the set support will ask for.

## The full list

[Error codes](/api-reference/errors) is generated from the code that raises them, so it
cannot drift from what you will actually receive.