# Errors

A failed request returns an HTTP status and a JSON body with one of six error tags and a message a person can act on.

A failed request returns a `4xx` or `5xx` status and a JSON body with two fields: `_tag`, which
names the kind of error, and `message`, a sentence that says what went wrong and, where it can,
what to do about it.

```json
{
  "message": "An archived product already uses the slug storefront. Restore it or choose another slug.",
  "_tag": "Conflict"
}
```

Branch on `_tag` or the status, never on the message: messages are written for people and can
change.

## Error tags

| Status | `_tag`         | Meaning                                                                                                              |
| ------ | -------------- | -------------------------------------------------------------------------------------------------------------------- |
| `400`  | `InvalidInput` | The request is malformed, or a value is not allowed, such as a slug with capitals or a body missing a required field |
| `401`  | `Unauthorized` | The request has no valid API key: it is missing, unknown or revoked                                                  |
| `403`  | `Forbidden`    | The key's user or role may not do this, such as a viewer creating a ticket or a member writing a production secret   |
| `404`  | `NotFound`     | The workspace or object does not exist, or the caller cannot see it                                                  |
| `409`  | `Conflict`     | The change conflicts with current data, such as a slug that is already in use or an archived object                  |
| `500`  | `Internal`     | Clanker Ship failed to handle the request                                                                            |

Each endpoint in the reference lists the tags it can return.

## What each one means for you

- **`InvalidInput`** names the rule in its message, such as
  `Use 2–48 lowercase letters, numbers or hyphens, …`. A body that does not match the endpoint's
  schema at all, such as a missing required field or a value of the wrong type, gets the message
  `Invalid request payload`. Fix the request before sending it again.
- **`Unauthorized`** means the key itself failed. Check that you sent it, and that it was not
  revoked. See [Authentication](https://docs.qa.clankership.net/api/authentication.md).
- **`Forbidden`** means the key works but its role or kind does not allow the action, such as an
  account API key commenting on a ticket. Use a key with the right role.
- **`NotFound`** covers objects in other workspaces too: a workspace you do not belong to, or
  another workspace's product, returns `404`, never `403`, so the API does not reveal what exists.
- **`Conflict`** means the request was valid but the current data stops it: a taken name, an
  archived product, a test run that already finished, or a secret that changed since you read it.
  Read the object again, then decide. Repeating the same request gets the same answer.
- **`Internal`** means Clanker Ship failed. Retry after a short wait; before retrying a write that
  is not [idempotent](https://docs.qa.clankership.net/api/idempotent-requests.md), such as creating a ticket, check whether the first
  attempt went through. If it keeps happening, report it; see [Request IDs](https://docs.qa.clankership.net/api/request-ids.md).

## Handle errors in code

```typescript
const response = await fetch("https://api.clankertest.com/v1/workspaces/acme/products", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CLANKER_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ slug: "storefront", name: "Storefront", type: "website" }),
});

if (!response.ok) {
  const error = (await response.json()) as { _tag: string; message: string };
  if (error._tag === "Conflict") {
    // Already there: read it instead of creating it.
  } else {
    throw new Error(`${response.status} ${error._tag}: ${error.message}`);
  }
}
```

## Other shapes

- **A path that matches no endpoint** returns `404` with an empty body.
- **Test-run file uploads and downloads** (`/test-runs/{run}/files/{path}`) return their errors as
  `{ "message": "…" }`, without `_tag`.
- **The agent HTTP API and MCP** use their own shape, `{ "code", "message", "requestId" }`, with
  codes such as `not_found` and `permission_denied`; see [Agent HTTP API](https://docs.qa.clankership.net/mcp/agent-http.md#errors).
