---
title: "Errors"
description: "The error envelope, every code, and which are worth retrying."
canonical: "https://atelier201.com/docs/api/errors"
updated: "2026-11-06"
---

# Errors

Every failure has the same shape:

```json
{
  "error": {
    "type": "not_found_error",
    "code": "resource_not_found",
    "message": "This site does not exist.",
    "docs_url": "https://atelier201.com/docs/api/errors#resource_not_found"
  },
  "request_id": "req_01K9X7"
}
```

**Branch on `code`, never on `message`.** Codes are contract; messages are
written for people and improve over time. Some errors add a `details` object
with what you need to act, such as the current revision after a write conflict.

## Which to retry

| Status | Retry?                                                                                                                                     |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
| 429    | Yes, after `Retry-After`.                                                                                                                  |
| 5xx    | Only for an idempotent request (GET, PUT, DELETE). A POST that fails with a 5xx may already have been applied. Re-read the resource first. |
| 4xx    | No. The request is wrong and will be wrong again.                                                                                          |

## Every code

### authentication\_required

`authentication_error` · HTTP 401

No credential was sent. Add an `Authorization: Bearer` header.

### invalid\_api\_key

`authentication_error` · HTTP 401

The credential is not recognised. It may belong to a workspace that no longer exists.

### api\_key\_expired

`authentication_error` · HTTP 401

The key passed its expiry date. Create a new one.

### api\_key\_revoked

`authentication_error` · HTTP 401

Somebody revoked it. This is permanent.

### insufficient\_scope

`permission_error` · HTTP 403

The credential is valid but was not granted the scope this endpoint needs. The message names it. Re-issue the credential; the scope cannot be widened in place.

### feature\_not\_available

`permission_error` · HTTP 403

The plan does not include this. Publishing a draft answers this: `details.blockers` holds `plan_required`, and `action_url` opens the site's plan.

### admin\_required

`permission_error` · HTTP 403

The action is restricted to workspace admins: publishing, rolling back, taking a site offline, deleting a site and changing its domains.

### invalid\_request

`invalid_request_error` · HTTP 400

The body or the query did not validate. The message says which field.

### unknown\_field

`invalid_request_error` · HTTP 400

A field was sent that this endpoint does not accept. Inputs are strict on purpose: a silently ignored field is a bug you would find much later.

### invalid\_parameter

`invalid_request_error` · HTTP 400

A parameter was the wrong shape, usually a malformed id.

### invalid\_cursor

`invalid_request_error` · HTTP 400

The pagination cursor is not one we issued. Start the list again.

### request\_too\_large

`invalid_request_error` · HTTP 413

The body exceeded the hard limit.

### resource\_not\_found

`not_found_error` · HTTP 404

No such object in THIS workspace. A resource belonging to another workspace returns this too, deliberately: distinguishing them would let you discover which ids exist.

### conflict

`conflict_error` · HTTP 409

The resource exists and the request is well formed, but its current state forbids the action. Unlike a 400, this may succeed later. Common causes:

- A write conflict. Someone changed a file since you read it. `details.reason` is `revision_conflict`, and `details.current_revision` is the revision to read, and `details.paths` lists each file with the hash you expected and the hash found.
- A publish against a stale review, or a build that cannot go live. Review again, and read `details.blockers`.
- A plan limit, such as too many drafts or a domain beyond the plan. The message names the limit, and `action_url` the page that lifts it.

### result\_not\_ready

`conflict_error` · HTTP 409

The work has not finished, for example a site that is still being created. Wait a few seconds rather than retrying immediately.

### idempotency\_key\_reused

`conflict_error` · HTTP 409

The `Idempotency-Key` you sent to an endpoint that accepts one was already used for a different request in this workspace. Nothing was done. Use a new key for new work; a repeat of the same request with the same key returns the first answer instead.

### insufficient\_credits

`invalid_request_error` · HTTP 402

Not enough credits. **Stop and tell a human**; retrying will not help.

### request\_too\_large\_for\_plan

`invalid_request_error` · HTTP 422

The body is within the hard limit but above what this plan allows.

### result\_expired

`not_found_error` · HTTP 410

The result was deleted by the retention policy. It is not coming back.

### rate\_limited

`rate_limit_error` · HTTP 429

Too many requests. Wait for `Retry-After` before trying again. The same wait is in the body, as data: `details.retry_after_seconds` and `details.retry_at` (an ISO date), also in MCP tool errors and in the CLI's JSON output.

### internal\_error

`api_error` · HTTP 500

Something failed on our side. Quote the `request_id`.

### service\_unavailable

`api_error` · HTTP 503

A dependency is down or in maintenance. Retry after a delay.
