---
title: Errors
description: One envelope, a stable code to branch on, and a message written to be acted on.
---

## The envelope

Every failure, at every status, looks like this:

```json
{
  "error": {
    "type": "not_found_error",
    "code": "unknown_kb_article",
    "message": "This knowledge base has no published article at \"instal-the-cli\". Did you mean \"install-the-cli\"? List the published articles with GET /v1/kb/articles.",
    "request_id": "6e5c4029-5e1b-47fd-a72e-eb2d5b8f1e2c",
    "param": "slug",
    "did_you_mean": "install-the-cli",
    "doc_url": "https://docs.zevcontent.com/errors/unknown_kb_article"
  }
}
```

| Key | |
|---|---|
| `type` | The broad class. One of the seven below. |
| `code` | The specific reason. **Branch on this**, because it is stable. |
| `message` | A sentence saying what to do next. Written for a person reading a log; may be reworded. |
| `request_id` | Also on every response as `X-Request-Id`. Quote it when asking us. |
| `param` | Which parameter or field was at fault. |
| `did_you_mean` | **One** closest match, as a string. Not a list. |
| `available_fields` | Every field key a type declares, on a filter or sort naming one it does not have. |
| `available_types` | Every content type slug, on an unknown type. |
| `expected` / `got` | The type a value should have had, and the one it had. |
| `doc_url` | The page explaining this code. |

## Types

| `type` | Status | |
|---|---|---|
| `authentication_error` | 401 | We do not know who you are |
| `permission_error` | 403 | We do, and this is not allowed |
| `not_found_error` | 404 | No such thing |
| `conflict_error` | 409 | It exists, or it is in use |
| `rate_limit_error` | 429 | Too many requests |
| `invalid_request_error` | 4xx | The request was malformed |
| `api_error` | 5xx | Our fault |

## Errors are written to teach

The messages are not status codes restated. Naming something that does not
exist answers with the ones that do:

```
GET /v1/content/homepage
```
```json
{
  "error": {
    "code": "unknown_content_type",
    "message": "This project has no content type with slug \"homepage\". Available types: homepage-hero, filter-demo.",
    "param": "typeSlug",
    "available_types": ["homepage-hero", "filter-demo"]
  }
}
```

> **INFO: An empty list is never a typo**
>
> A filter that matches nothing is `200` with an empty array. A *name* that does
> not exist is a `4xx`. Relying on this distinction is the difference between "we
> have no posts in Engineering" and "you spelled Engineering wrong".

## Codes you will actually meet

### Reading

| `code` | |
|---|---|
| `unknown_content_type` | No type with that slug. Carries `available_types`. |
| `not_a_collection` / `not_a_singleton` | Right type, wrong kind for the endpoint. |
| `entry_not_published` · `content_not_published` | It exists and is a draft. |
| `unknown_entry` | No entry with that id in this type and release. |
| `unknown_release` | No release with that `rel_…` id. |

### Blog and knowledge base

| `code` | |
|---|---|
| `unknown_blog_article` · `unknown_kb_article` | No such slug. Usually carries `did_you_mean`. |
| `blog_article_not_published` · `kb_article_not_published` | It exists and is a draft. Publish it. |
| `kb_article_archived` | Retired on purpose. Restore it, or remove the link. |
| `unknown_blog_category` · `unknown_kb_collection` · `unknown_kb_section` | A filter named something that does not exist. |
| `unknown_author` | No author with that slug. |

### Filtering and paging

| `code` | |
|---|---|
| `invalid_limit` | Outside 1–100. Refused, not clamped. |
| `invalid_offset` | Not a whole number ≥ 0. |
| `invalid_cursor` | Not a cursor we issued. Do not construct them. |
| `unknown_filter_field` | Carries `available_fields`. |
| `unsupported_filter_operator` | The operator does not apply to that field type. |
| `unknown_sort_field` · `unsortable_field` | Sorting by something that cannot be sorted. |
| `too_many_ids` | More than 50 in `?ids=`. |

### Writing

| `code` | |
|---|---|
| `secret_key_required` | A write attempted with a publishable key. |
| `missing_required_field` · `invalid_field_value` | Validation against the type. |
| `invalid_richtext_document` · `unknown_richtext_node` | The document did not match the schema. |
| `blog_slug_taken` · `kb_slug_taken` | Another article already has that slug. |
| `content_type_kind_immutable` · `field_type_immutable` | A change that would break readers. |

### Everything else

| `code` | |
|---|---|
| `origin_not_allowed` | A publishable key from an unlisted origin. |
| `rate_limited` | See [Rate limits](/api/rate-limits). |
| `permanent_media_urls_unavailable` | `media_urls=permanent` on a private project. |
| `internal_error` | Ours. Quote the `request_id`. |

## 5xx

A `500` carries a generic message on purpose. An internal error's text can leak
query shapes and table names, so it is logged in full and reported as
`internal_error`. The `request_id` is the link between the two: quote it and we
can find the exact request.