---
title: Errors
description: The one shape of an AdCrunch failure, the field to branch on, the order of the checks, and the codes that more than one operation sends.
---

## The shape of a failure

Each AdCrunch failure has the same JSON body, in each of the four APIs:

```json
{
  "error": "not_found",
  "message": "Skill not found"
}
```

- **`error` is a stable code.** Branch on it. A code does not change for a given failure.
- **`message` is for a person.** Show it, or log it. AdCrunch rewrites a message when it can say it better, so never branch on it.

Some failures add a field that tells you what to do next:

| `error` | Added field | What it holds |
| --- | --- | --- |
| `revision_mismatch` | `currentRevision` | The revision that the row holds now. See [Edit without overwriting](/api/edit-without-overwriting). |
| `slug_conflict` | `slug` | The slug that is already taken. |
| `unsupported_type` | `allowed` | Each media type that the operation accepts. Only when you reserve a Document. |
| `invalid_request` | `issues` | One entry for each field that does not match. See [A request that does not match](#a-request-that-does-not-match). |
| `too_large` | `sizeBytes`, `limitBytes` | The size of the stored file, and the limit. Only when you finalize a Document. |
| `live_read_unsupported`, `provider_not_connected`, `provider_error` | `provider` | The provider of the advertiser. See [Live reads](#live-reads). |
| `provider_error` | `platformError` | The error object of the provider, with no change. |

The page of each operation lists each status and each code that it sends. The [overview](/api) lists each operation.

## A request that does not match

A request that does not match the schema of its operation answers `400` with `error: "invalid_request"`. The field is missing, it has the wrong type, or the body is not valid JSON. `issues` names each field. This is `POST /assets/uploads` with no `filename`:

```json
{
  "error": "invalid_request",
  "message": "The request does not match the schema. Read issues for each field.",
  "issues": [
    {
      "in": "body",
      "path": "/filename",
      "message": "Invalid input: expected string, received undefined"
    }
  ]
}
```

- **`in`** is the part of the request that holds the field: `body`, `query`, `params`, `headers` or `cookie`.
- **`path`** is a JSON Pointer into that part. An empty string is the whole part, for example a body that is not valid JSON.
- **`message`** is for a person, as above.

The answer never repeats what you sent. The schema of each operation is on its page.

## `400` and `422`

- **`400`**: the request does not have the right shape. `invalid_request` is the code. Correct the request.
- **`422`**: the request has the right shape, and AdCrunch refuses what it says. The code says why, for example `invalid_amount` or `advertiser_not_owned`.

## The order of the checks

Each operation checks the API key first, then the permission, then the request. So a request with no key and a bad body answers `401`, and a key without the permission answers `403`, before the body is read against the schema.

There is one exception. A body that is not valid JSON answers `400` before the API key is checked, because the body is read before any check runs.

## A path that does not exist

A path that no operation answers gets `404` with `error: "not_found"`, the same code as a thing that does not exist. The `message` tells the two apart.

**Changed on 2026-09-24.** Before this date, a request that did not match the schema answered `422` with the shape of the framework, and a path that did not exist answered the plain text `NOT_FOUND`. Neither had an `error` field. In Observe, Context, and Assets, the schema was checked before the API key.

## The data of another organization

The API never tells you whether a thing exists in another organization.

- **Mutations, Context, and Assets** answer `404` with `error: "not_found"`. A thing that never existed gets the same answer. So these APIs never answer `403` for it.
- **Observe** answers `200`. A list is empty, and a read of one entity has an empty body. In Observe, an advertiser of another organization is not a failure.

## Codes that more than one operation sends

| Status | `error` | Meaning | What to do |
| --- | --- | --- | --- |
| `400` | `invalid_request` | The request does not match the schema. `issues` names each field. Every operation that takes input. | Correct the request. |
| `400` | `invalid_cursor` | AdCrunch cannot use the `cursor`: it cannot read it, or the cursor belongs to a different query. Every list of Observe, Mutations, Assets and Context. | Send the `nextCursor` of the previous answer, with no change. |
| `401` | `unauthorized` | No API key, or a key that does not resolve. | See [Authentication](/api/authentication). |
| `403` | `forbidden` | The key does not hold the permission. | See [Authentication](/api/authentication). |
| `404` | `not_found` | The thing does not exist in your organization, or no operation answers the path. | Do not retry. |
| `409` | `revision_mismatch` | Somebody wrote first. | Read again, apply your change, and send it again. |
| `409` | `slug_conflict` | Another row in your organization has that slug. | Choose a different slug. |
| `413` | `too_large` | The stored file is larger than the limit. AdCrunch deleted it. | See [Upload a file](/api/upload-a-file). |
| `415` | `unsupported_type` | The stored file has a type that AdCrunch does not accept. AdCrunch deleted it. | See [Upload a file](/api/upload-a-file). |
| `415` | `type_mismatch` | The bytes of a stored Asset are a different type from the `Content-Type` of its upload. AdCrunch deleted it. | Upload again with the real `Content-Type`. |
| `415` | `unreadable` | The stored Asset is no accepted type, or it ends before its header does. AdCrunch deleted it. | Upload an intact file of an accepted type. |
| `422` | `advertiser_not_owned` | That advertiser is not one of your organization. | Use an `advertiserId` of your organization. |

## Live reads

Every Observe operation whose path names an advertiser, such as `GET /observe/{advertiserId}/campaigns`, is a live read: AdCrunch asks the provider itself, and not what AdCrunch holds. A live read can fail in three ways, and each needs a different action:

| Status | `error` | Meaning | What to do |
| --- | --- | --- | --- |
| `501` | `live_read_unsupported` | AdCrunch cannot read this resource live from this provider. This does not change. | Read the stored copy: `GET /observe/campaigns?advertiserId=acc_…`. |
| `409` | `provider_not_connected` | The connection to the provider does not work. | A person must connect the advertiser again. The stored read, `GET /observe/campaigns?advertiserId=acc_…`, still answers what AdCrunch holds. |
| `502` | `provider_error` | The provider refused the request, or did not answer. | Retry. This failure is often temporary. |

[What each provider supports](/connect/providers) lists which resource each provider can read live.
