---
title: Errors
description: The one shape of a failed tool call — the code in structuredContent and the sentence in the text — the codes that each tool can send, and what to do next.
---

A tool call that fails does not fail over HTTP. The tool runs, refuses, and returns a normal tool result with `isError` set. That result has one shape for each tool. [Errors](/api/errors) in the API reference describes the REST API, which uses the same codes.

## The shape

```json
{
  "content": [
    {
      "type": "text",
      "text": "Brand \"acme-running\" was changed since you read it (current revision 7). Re-fetch with brand_get and retry with the latest base_revision."
    }
  ],
  "isError": true,
  "structuredContent": {
    "error": "revision_mismatch",
    "message": "Brand \"acme-running\" was changed since you read it (current revision 7). Re-fetch with brand_get and retry with the latest base_revision.",
    "currentRevision": 7
  }
}
```

- **`isError`** is `true`. The tool ran and refused.
- **The text block** holds one sentence for a person. The agent reads it, as it reads a successful result.
- **`structuredContent`** holds the same failure as data, in the shape of each AdCrunch API:

| Field | What it is | Branch on it? |
| --- | --- | --- |
| `error` | A stable code, lower snake_case. | **Yes. Only this.** |
| `message` | The sentence of the text block. | Never. |
| Extension fields | Facts that make the failure actionable, such as `currentRevision` above. | Yes, for the code that sends them. |

`message` carries no contract. AdCrunch rewrites a sentence when it can say it better. That is why the code is a separate field. A client that reads only the text still gets the whole failure.

:::tip[Always write a default branch]

A code that you have never seen is what a new failure looks like. AdCrunch can add a code to a tool at any time. A code that changes its name or goes away breaks callers, so that does not happen without notice.

:::

## The codes that each tool can send

Each tool can send these three codes:

| `error` | Extension fields | What it means | Do this |
| --- | --- | --- | --- |
| `forbidden` | — | The token of your client does not hold the scope of this tool. | Authorize your client again, and grant the scope. [Auth & scopes](/mcp/auth#scopes) lists each scope. |
| `invalid_request` | `issues` | The arguments do not match the input schema of the tool, or they break a rule over several arguments. `issues` names each argument. Nothing changed. | Correct the arguments. The same call fails again. |
| `internal_error` | — | AdCrunch could not answer, or its answer did not match the output schema of the tool. The sentence never holds the details of the fault. | Try again later. |

Each tool also sends codes of its own, such as `not_found` or `revision_mismatch`. The page of each tool lists all the codes that it can send, under its reference part. For an example, see [`brand_update`](/mcp/tools/brand-update). The [six job guides](/mcp/overview#the-six-jobs) link to each tool page. The tool list gives the same codes in the `_meta` of each tool, under `dev.adcrunch/errors`.

One failure has one code on MCP and on the REST API. When a REST operation sends the same failure, the tool sends the same code, with the same extension fields.

### An argument that does not match

`issues` has one entry for each argument that does not match the schema:

```json
"issues": [
  {
    "path": "/base_revision",
    "message": "Invalid input: expected number, received undefined"
  }
]
```

- **`path`** is a JSON Pointer into the arguments. An empty string is the whole set of arguments, for a rule over several arguments such as "at most one of `dailyBudget` and `lifetimeBudget`".
- **`message`** is for a person. Never branch on it.

A tool call has one part, its arguments. So an entry has no `in` field, which the REST API uses to name a part of the HTTP request.

## An edit that collides

`revision_mismatch` means that the object changed after the agent read it. Its extension field `currentRevision` gives the revision of now. Nothing changed. The agent handles this failure, and does not only report it. [What to expect](/mcp/what-to-expect#an-edit-can-collide-with-another-edit) states what the agent then does.

## A missing thing and a thing that is not yours

Each tool reads your organization only. So an id that does not exist and an id of another organization get the same answer: `not_found`, or an empty list.

This is deliberate. An answer that told the two apart would let a caller find which ids exist.

Two results follow:

- **`not_found` does not prove that somebody deleted the thing.** It can exist in an organization that you are not in.
- **An empty result from [`query_insights`](/mcp/tools/query-insights) has two meanings.** Either your organization does not own that advertiser, or the advertiser has no data for that period. The result cannot tell you which.

A tool whose name starts with a provider, such as `meta_list_pixels` or `meta_set_status`, takes the advertisers of that provider only. An advertiser of another provider gets `not_found` too.

The agent usually recovers by itself. It reads the list again, and it retries with a real id.

## Two failures that are not tool results

Two failures stop each tool at once, before a tool runs. They are HTTP answers of the endpoint, and your client shows them as a connection problem:

- **`401`**: the token is missing, has expired, or does not verify.
- **`403`** with `insufficient_scope`: the token holds no AdCrunch scope at all.

Both answers carry a `WWW-Authenticate` header that tells your client how to authorize again. For both, authorize AdCrunch again from the settings of your client. [Another client](/mcp/setup/other-clients#when-the-endpoint-refuses-the-token) gives the details for a developer.

## A change that fails is not a failed tool call

A change on Meta runs after the tool call, in a queue. [`get_mutation_status`](/mcp/tools/get-mutation-status) reports how it ended. When AdCrunch or Meta refused the change, the status is `complete`, and `result.error` holds the code, for example `budget_cap_exceeded`.

That call is a success: it read the status, and the status says that the change failed. So `isError` is not set. The agent reads the code in the result, and tells you why the change did not happen.

## What's next

- [Auth & scopes](/mcp/auth): what a token can touch.
- [What to expect](/mcp/what-to-expect): the rules that each tool follows.
- [Errors](/api/errors) in the API reference: the same codes on the REST API.
