Errors
The one shape every AdCrunch failure arrives in, every status code you can meet, what is safe to retry, and how the same failure reaches an MCP client.
On this page
Every published AdCrunch API answers every failure with the same shape. Read this page once and you can write your error handling before you write your first call.
The shape
{
"error": "revision_mismatch",
"message": "This brand changed since you opened it",
"currentRevision": 7
}
Two fields are always there, and they do different jobs.
| Field | What it is | Branch on it? |
|---|---|---|
error |
A stable code, lower snake_case. | Yes. Only this. |
message |
One sentence, written for a person to read. | Never. |
message carries no contract. AdCrunch rewords it whenever it can be said better, and that is the whole reason the two are separate fields. If you match on prose, an improvement to the wording breaks your integration.
Some codes carry an extension field beside the two, and that field is what makes the failure actionable — currentRevision above tells you what to read before you retry.
Tip
Always write a default branch
A code you have never seen is what a new failure looks like. AdCrunch may add a code to an operation at any time — that is additive, and it is why every operation’s list of codes is open. Renaming or removing a code is breaking, and it does not happen without notice.
The codes
| Status | error |
Extension | What it means |
|---|---|---|---|
400 |
invalid_request |
— | The body or query does not match the operation. Nothing was started. |
400 |
invalid_cursor |
— | That pagination cursor could not be read. Pass back the nextCursor you received, unchanged. |
401 |
unauthorized |
— | No credential, or one that does not resolve. |
403 |
forbidden |
— | Your credential resolved, and it does not hold the permission this operation needs. |
403 |
missing_write_access |
— | The advertiser is connected without write access. Reconnect the provider to grant it. |
404 |
not_found |
— | No such thing for your organization. See below. |
404 |
not_attached |
— | That advertiser is not attached to this brand. |
409 |
revision_mismatch |
currentRevision |
Someone changed it since you read it. |
409 |
slug_conflict |
slug |
Another row of the same kind already uses that slug. |
409 |
no_object |
— | The upload was reserved and the bytes never arrived. |
409 |
currency_frozen |
currency |
The Campaign Plan is denominated, and a line item already carries a budget. Clear those budgets to change the currency. Nothing is converted. |
409 |
persona_not_in_brand |
lineItemIds |
You re-pointed a Campaign Plan at another brand, and the listed line items still name a persona of the old one. Clear those personas first. |
413 |
too_large |
limitBytes, sizeBytes |
The file is over the limit. |
415 |
unsupported_type |
allowed |
That media type is not accepted. allowed lists the ones that are. |
422 |
advertiser_not_owned |
— | That ad account is not connected to your organization. See below. |
422 |
invalid_age_range |
— | A persona’s age bounds are whole numbers, and age_min may not exceed age_max. |
422 |
invalid_amount |
— | Amounts are whole numbers of minor units, and never negative. |
422 |
invalid_countries |
— | Countries are ISO 3166-1 alpha-2 codes, such as ["FR", "BE"]. |
422 |
invalid_currency |
— | A currency is a three-letter ISO 4217 code, such as EUR. |
422 |
invalid_logo |
— | The document you nominated as a logo is not one this brand owns. |
422 |
invalid_slug |
— | The slug you sent holds no letter or digit. |
422 |
invalid_window |
— | Dates are real YYYY-MM-DD dates, and the start may not follow the end. |
422 |
persona_not_in_brand |
— | The persona you named on a line item belongs to another brand. A plan with no brand has none available. |
Two of these codes carry two statuses, because they answer two different questions:
persona_not_in_brandis422when you name the wrong persona on one line item, and409— withlineItemIds— when you move a whole plan to another brand and its existing rows still point at the old one.not_foundis what you get for a thing you named that your organization does not have.advertiser_not_ownedis422, not404, because an ad account is an input to an operation rather than the thing the operation reads.
This page covers what AdCrunch publishes. Brand enrichment is reachable and deliberately undocumented, and it sends two codes that are not listed here.
The one failure that looks different
A request that fails the operation’s own schema is rejected by the framework before any AdCrunch code runs. That body is the framework’s, not ours:
{
"type": "validation",
"on": "body",
"property": "budget",
"message": "Expected number",
"found": { "…": "…" },
"errors": [{ "…": "…" }]
}
It arrives as 422 and it has no error field. Documented rather than translated, so that you meet it here rather than in production. Treat a 422 with a type of validation as “I sent the wrong shape”, and read property to find out where.
What to retry
| Status | Retry? | Do this instead |
|---|---|---|
400, 422 |
No | Fix the request. Retrying sends the same wrong thing. |
401 |
Not as-is | Refresh the token, or authorize AdCrunch again from your client’s settings. Then retry once. |
403 |
No | The credential is right and its permissions are not. Change the scope or reconnect the provider. |
404 |
No | Re-read the parent listing. The id may be stale, or it may never have been yours. |
409 revision_mismatch |
Yes, after a read | Read the object again, apply your change to the version that came back, and send it with the new revision. |
409 (any other) |
No | slug_conflict, no_object, currency_frozen and persona_not_in_brand each describe a state you must change first. The same request will fail again. |
413, 415 |
No | Change the file. |
5xx |
Yes, with backoff | Something failed on our side or on the provider’s. Nothing was necessarily left half-done — check the status of what you started before you start it again. |
A 409 revision_mismatch is the one worth handling rather than surfacing. Read the object again, fold your edit into the version that came back, and send it with the new revision. To read the whole object again and start over discards what the author had typed.
A missing thing and a thing that is not yours
Read one thing, and the two answers are the same. Every read is scoped to your organization, in the query itself, so an id that does not exist and an id that belongs to another organization both answer 404 not_found — or, for a list, an empty list.
This is deliberate. A surface that told the two apart lets a caller find which ids exist by asking for them.
Two consequences:
- A
404is not proof the thing was deleted. It can be alive in an organization you are not in. - An empty result from
query_insightshas two meanings. Either your organization does not own that advertiser, or the advertiser reports no data for that range. The response cannot tell you which.
Name an ad account, and the answer is different. An operation that takes an advertiserId as an input — attach it to a brand, put it on a line item, record an execution — answers 422 advertiser_not_owned rather than 404.
That is not a hole in the rule above. The answer states one fact about your organization: this ad account is not connected to it. It says nothing about whether the account exists anywhere else, so there is nothing for a caller to find by asking.
How a failure reaches an MCP client
An MCP tool call is not an HTTP request from your agent’s point of view, so there is no status code to read. A tool that fails returns a normal tool result with isError set, and a sentence in its text content:
{
"content": [
{ "type": "text", "text": "Entity not found: meta/campaign/123." }
],
"isError": true
}
Your agent reads that sentence, the same way it reads a successful result. In practice this means:
- The sentence is the whole error. There is no code beside it on the MCP surface today.
- The agent will usually recover on its own — it re-reads the parent list and tries again with a real id. That is the behaviour to expect, not a bug.
- A tool result is not a failed call.
isError: truemeans the tool ran and refused. A transport-level failure is something else, and your client reports it as one.
An OAuth token that has expired is the one failure that stops every tool at once. Authorize AdCrunch again from your client’s settings.
What’s next
- Auth & scopes — what a credential is allowed to touch in the first place.
- API reference — every operation, with the codes it can send.
- Concepts — the words these messages use.