Skip to main content
The v2 REST API uses a single, predictable error envelope across every endpoint. Whatever the failure — a missing auth token, a Zod validation problem, a 404, a downstream rate limit — the body shape is the same. Build your error-handling once and reuse it everywhere.
This page covers v2 REST endpoints. MCP tool errors follow the ADCP error spec and are returned in structuredContent rather than HTTP status codes.

Error envelope

Every non-success response has data: null and a populated error object:
Successful responses have the inverse shape: { "data": <result>, "error": null }. List endpoints add a meta block (see Pagination).
Throughout the Buyer and Storefront task references, the ## Response examples show the data payload only — the inner result. On the wire it is always wrapped in the envelope above: { "data": <payload>, "error": null }, plus a meta block on list endpoints. Read the result from response.data.

HTTP status codes

Common error codes

Domain-specific codes you may encounter on campaign, currency, and reporting endpoints:

Buyer identity and storefront account errors

Buyer operations that need a seller-side account fail closed when the buyer has no confirmed operator identity. REST returns HTTP 409 with BUYER_SETUP_REQUIRED; for a missing identity, branch on this exact structured payload:
Interchange never falls back to interchange.io as a buyer operator. Doing so would collapse unrelated buyers into one seller-side natural-key account. Complete Buyer Setup and retry instead. Historical media buys are not rewritten: status, creative re-sync, update, cancellation, and delivery keep using the seller account persisted on the buy. A legacy buyer profile that explicitly contains the platform domain returns the same BUYER_SETUP_REQUIRED code with details.reason: "platform_operator_not_allowed". This is observable on the same buyer surfaces as missing_operator_identity; replace the platform domain in Buyer Setup rather than retrying it. The Storefront MCP surface uses the ADCP error envelope rather than the REST envelope above. When a request supplies an opaque account_id that is not authorized for the authenticated buyer on that storefront, it returns:
Do not interpret this as account_context_not_found, and do not retry with an account ID discovered by another buyer. Use list_accounts as the current buyer or complete the seller’s account-linking flow.

Buyer-actionable errors on create_media_buy, update_media_buy, sync_creatives

Buy-flow errors carry an optional buyer_reason: { code, message } sub-object on the ADCP error envelope (AdCP 3.2 core/error.json). It classifies the failure in a way the buyer can act on — creative-validation issues, budget/product/ permission problems — with a message that is safe to render directly (no vendor identifiers, no internal IDs, no stack traces). When buyer_reason is present, the envelope’s recovery (transient | correctable | terminal) is set alongside per spec and MUST agree with the buyer_reason’s classification. code uses the AdCP enums/error-code.json vocabulary (BUDGET_TOO_LOW, PRODUCT_NOT_FOUND, PRODUCT_UNAVAILABLE, PERMISSION_DENIED, ACCOUNT_SETUP_REQUIRED) or an X_{VENDOR}_{CODE} extension (Scope3 extensions: X_SCOPE3_CREATIVE_SIZE_MISMATCH, X_SCOPE3_CREATIVE_MISSING_CLICK_URL, X_SCOPE3_CREATIVE_VALIDATION_FAILED_GENERIC). Receivers MUST preserve unknown codes for forward compatibility — a newer AdCP standard code or a seller-defined extension is passed through end-to-end.
The same buyer_reason may also appear on each entry of details.per_source so a per-leg diagnosis is available when a fan-out mixed causes; each entry carries its own recovery peer. On sync_creatives, buyer_reason + recovery appear on each failed creative row’s errors[] entry when the source classified the batch failure. Prefer reading the structured buyer_reason.code for programmatic routing (retry, mutate-and-resubmit, escalate) and use buyer_reason.message for what to show a human.

MCP Page capability errors

Host-only Page aliases return the ADCP ACCESS_DENIED tool error when their session-bound capability is missing, expired, belongs to another Page or session, or the requested arguments fall outside that Page’s exact allowlist. The error is returned in MCP structuredContent, not the REST envelope. For an expired capability, the Page caller makes one renewal attempt with the tool associated with the same Page resource and, if renewal succeeds, retries the unchanged operation once. If renewal fails, preserve the original denial. Do not retry a wrong-resource or off-policy call through another Page. In Teach, ACCESS_DENIED also prevents the Page from rejecting a destination-owned Material candidate. Teach presents that candidate as a handoff. A direct MCP client may record rejection with public save_material; acceptance still goes through the candidate’s named canonical typed owner.

Validation errors

When request validation fails, code is VALIDATION_ERROR and details.issues enumerates every problem Zod found, with dotted field paths:
When a single-field check fails (e.g. a route guard), field is set instead:
Always render details.issues[].path in your UI — the user usually just needs to know which form field to fix.

Handling errors in client code

Don’t pattern-match on message text — message strings may be reworded for clarity. Always branch on error.code (and on HTTP status as a fallback).

Retrying safely

RATE_LIMITED, INTERNAL_ERROR, SERVICE_UNAVAILABLE, and FX_RATE_UNAVAILABLE are transient — retry GETs with exponential backoff. For creation/mutation requests after a 5xx, prefer to surface the error rather than auto-retry, since duplicate-create protection isn’t enforced server-side. VALIDATION_ERROR, NOT_FOUND, FORBIDDEN, ACCESS_DENIED, CONFLICT, and SPEND_DENOMINATION_UNRESOLVED are terminal — don’t retry until the input or state changes. CONFLICT has two narrow automatic-retry reasons, and they are machine-readable rather than a matter of reading the message. name_creation_in_progress means another writer holds that name and has not finished. adoption_in_progress means the source-identity state is still settling: another adoption is completing or its blocker changed during classification. Retry the identical request shortly for either reason. Every other adoption reason requires a changed input or state: name_taken and name_creation_abandoned require choosing another name or archiving the blocker; adoption_abandoned means a stale creation marker never cleared (an asset row may already exist), and adoption_asset_unavailable means the finalized asset is no longer usable; adoption_name_mismatch requires using the existing creative name or renaming it separately; and source_delivery_revoked or source_deletion_requested requires uploading a new source. MCP classifies those action-required cases as correctable, not transient. Branch on details.reason, never on the message.
A 5xx status does not by itself mean “retry”, and a 4xx does not mean “never”. On MCP surfaces, branch on the recovery classification (transient / correctable / terminal) that every error carries; on REST, branch on code. A read that fails because stored data is inconsistent — such as SPEND_DENOMINATION_UNRESOLVED — is terminal even though nothing about your request was wrong, and an automatic retry loop on it will simply burn quota.