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 hasdata: 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 HTTP409 with
BUYER_SETUP_REQUIRED; for a missing identity, branch on this exact structured
payload:
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:
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.
An account that sends account_id together with brand or operator never
reaches that check. It fails request validation with VALIDATION_ERROR,
field: "account", and details.issues[] entries for /account (the
additionalProperties and oneOf rules). Send either { "account_id": … } or
{ "brand": …, "operator": … }. On a seller in complete-key mode, see
Address your approved account from a direct AdCP 3.0 or 3.1 agent
for the full set of refusals.
When you launch a campaign on an Apostra seller whose inventory runs on
Apostra’s managed sales agent, the seller needs an account for the
advertiser. That account is created when the advertiser is activated for the
seller, so a launch that runs before activation finishes fails that buy with
ACCOUNT_SETUP_REQUIRED (recovery: "correctable") before anything reaches the
seller. The buy’s safeMessage and details.setup_steps name the step the
advertiser’s activation still needs:
- Markets and channels missing: activation cannot create the account until
the advertiser has markets and channels. Set
brandCountriesandchannelswithsave_advertiser. - Not enabled for the seller: turn the advertiser on for the seller with
save_connection({ sellerId, advertiserActivation: { advertiserId, decision: "ENABLED" } }). - Still being set up, or waiting for the seller’s approval: wait, then retry.
- Another required action, such as payment: complete it.
details.setup_urllinks to it when one exists.
get({ kind: "seller", id }) shows the
advertiser’s activation state as active. The buy is recorded as failed with
the same explanation, owned by the buyer (invalid_request).
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 (Apostra 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.
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.
For managed ad-server inventory, local targeting validation can return
buyer_reason.code: "unsupported_targeting" with recovery: "correctable".
Its message names the targeting field or product setting that needs attention.
Update that setting and submit a new media buy. Other provider, authentication,
and transport messages are not promoted into buyer-visible reasons.
Media-buy change with an unknown outcome
A media-buy update, pause, or cancel that reaches a connected ad platform can fail after the platform may already have applied part of it. When Apostra cannot confirm the change and cannot prove it was undone, the error is as follows. Today Meta and ChatGPT ads media buys report this outcome. Undoing a change is not proof on its own. If the platform timed out, returned a server error, or read back without the requested change, the original change can still land after Apostra’s undo. Apostra still attempts the undo, but it reports this outcome instead ofrolled_back.
details.reason and
details.retry_safe, not on the message.
This is the update counterpart of the create-time scope3_create_uncertain and
scope3_mutation_uncertain outcomes described in
Create media buys.
Media-buy request stopped before reaching the ad platform
When Apostra rejects acreate_media_buy or update_media_buy before sending
anything to the connected ad platform, the error carries
details.mutation_outcome: "not_dispatched". Nothing in the request was
applied on the platform. Pinterest, Snap, LinkedIn and ChatGPT ads media buys
report this outcome whenever Apostra can prove nothing was sent. A few rejections cannot be
proved, for example a failure while Apostra serializes concurrent changes to the
same account or media buy. Those carry no mutation_outcome.
update_media_buy: the key is released. Resending with the same key runs the request again instead of returning the saved failure. Fix what the error names first; an identical request usually fails the same way.create_media_buy: the key is released only whenrecoveryiscorrectable. Otherwise the same key keeps returning the saved failure, so send the corrected request with a new key.
create_media_buy that Apostra deleted from Snap after a later step
failed reports details.mutation_outcome: "rolled_back" instead: the request
reached Snap and was removed. The idempotency key is kept.
A ChatGPT ads media buy does the same. A create_media_buy that failed after
the campaign was written reports rolled_back once Apostra has archived the
campaign and read the archive back, and unknown when it could not. A
update_media_buy that failed after part of it was applied puts back the
campaign budget and targeting, the ad group settings and the creative it
changed, and reports rolled_back when the platform reads back the original
values. If the campaign was active, it stays paused after the failed update;
resume it once you have checked the buy.
An error without mutation_outcome may have reached the ad platform. Do not
assume a retry is safe; read the media buy’s current state first. Branch on
details.mutation_outcome and recovery, not on the message.
Flight window elapsed on an approval-gated storefront
Some publisher storefronts hold an incomingcreate_media_buy for a human
operator to approve before it reaches the seller. Approval and dispatch are
separate moments: the buy waits in the operator’s queue, and only once it is
approved does the platform send it on. That wait can be long — minutes while
an operator reviews, or longer if the publisher has paused intake.
If a buy’s end_time has passed by the time it is dispatched, no seller can
accept it. The platform stops before contacting any seller and fails the buy’s
task with details.forward_error.code of flight_window_elapsed:
create_media_buy with a current
end_time. Retrying the same buy will not help and is not attempted on your
behalf: this media buy is finished, and the platform will not send it to a
seller under any later condition. The recovery of correctable refers to the
request — a corrected flight window succeeds — not to the buy that failed.
Why you may not have seen it before. A buy in this state used to be retried
against the seller for several hours, rejected each time, and then left
approved but never placed with no reason recorded. It now fails on the first
dispatch attempt with the message above, so the flight window is the thing you
fix rather than something you have to infer from silence.
A
start_time that has merely slipped into the past is not an error. The
platform re-reads the flight window at dispatch and sends a past start as
"asap" — start on acceptance — so a buy whose start passed while it sat in
the approval queue still runs. Only an elapsed end_time ends the buy.Approved buy that could not be sent yet
After an operator approves a buy on an approval-gated storefront, sending it to the seller can fail for a reason that may clear on its own or once someone acts, for example a seller that has not yet linked your account to its inventory source, an advertiser not yet mapped to an ad account on a storefront that books through an ad platform such as Meta or TikTok, or a seller that is briefly unreachable. The platform retries the same buy for up to six hours; time the publisher spends with intake paused does not count. While it retries, the buy’screate_media_buy task stays
submitted and the buy stays pending. When a
later attempt succeeds, the task completes as normal. Do not resubmit the buy
during this window: a new create_media_buy creates a second buy.
If no attempt succeeds before the window ends, the platform stops retrying and
the task fails:
correctable refers to a corrected, new request, not to the media buy that
failed.
A failure that no retry can fix, such as every source refusing the request
itself, fails the task on the first attempt with that diagnosis, the masked
details.per_source, and no retry_after.
Approved update that could not be sent yet
Anupdate_media_buy that an operator approves is sent to the seller the same
way, and follows the same rules. While the platform retries it, for up to six
hours not counting time the publisher spends with intake paused, the update’s
task stays submitted. That includes an update that has to wait for the media
buy itself to settle at the seller, such as a buy the seller is still accepting
or a cancellation still in progress. A later successful attempt completes the
task. Do not submit the same change again during this window.
If no attempt succeeds before the window ends, the platform stops retrying and
the update’s task fails:
INVALID_REQUEST, and every source refusing the update
fails with that diagnosis and the masked details.per_source. Neither carries
retry_after. Except for a cancellation, the platform does not send an update
again after its task has failed, so submitting it again does not apply it
twice. A cancellation that
cannot be sent leaves the media buy running; the seller sees the blocked
cancellation in its approval queue.
Plan-gated refusals
When Apostra refuses an action because of an organization’s plan, the message says the plan is the reason, anddetails.planSwitch gives a typed next step so
an agent can tell the person to switch plans instead of guessing. The error code
and the other details fields of each refusal do not change. The field is added
beside them on REST responses and in MCP structuredContent.
These refusals carry it:
- The organization’s plan does not cover the change
(
details.reasonplan_does_not_cover_account). When the first step is to pick a different seller product, there is no plan switch. - Custom modular inventory sources need an Enterprise Merchandising plan
(
403,FEATURE_NOT_ENABLEDon REST). - Operating agents for clients needs an active Certified Agent Partner Program entitlement.
- A Seller has not selected a plan (
INVALID_REQUEST,details.reasonplan_selection_required). The caller cannot switch another organization’s plan, socanActisfalseand there is no link. - A Seller has not met the prerequisites for running briefs
(
INVALID_REQUEST,details.reasonseller_prerequisites_missing).details.missinglistsbrief_acceptance_rules,payment_authorityorstorefront_unmatched(the storefront handling the brief is not the Seller’s), anddetails.prerequisitesgives each step’snextStep.canActisfalsefor the caller, and the Seller’s administrator completes the steps (see Before your storefront runs briefs).
Platform policy refusal
When the platform safety policy declines a discovery brief or a campaign write, the error isINVALID_REQUEST (HTTP 422) with the message “Request declined
by platform policy.”, details.reason of safety_refused, and
details.policy_documentation_url. It never names the matched rule. Revise the
request and retry, or contact support to ask for a review. If the check cannot
run, the error is a retryable SERVICE_UNAVAILABLE (HTTP 503); for a
discovery brief, details.reason is safety_check_unavailable. That is not a
finding about the request: retry it after a short wait. See
Request declined by platform policy.
Account type refusal
A Buyer API call needs a Buyer account, and a Seller API call needs a Seller account. A call from any other account type is refused, and the error says which. Branch ondetails, not on the message:
details.reasonisaccount_type_not_allowedwhen the account type is refused, andaccount_type_unverifiedwhen the platform could not confirm the account type. In the second case nothing was decided: retry after a short wait.details.surfaceisbuyerorstorefront, the API the call was refused on.details.accountTypeis the refused account’s type:parent(an organization, which is a container and not a Buyer or Seller account),developer,buyerorseller. It is absent whenreasonisaccount_type_unverified.
403 CUSTOMER_ROLE_DENIED (503 CUSTOMER_ROLE_UNAVAILABLE
for account_type_unverified). On MCP it is the ADCP ACCESS_DENIED tool error
in structuredContent. To act, switch to an account of the right type, for
example a child account inside the organization.
MCP Page capability errors
Host-only Page aliases return the ADCPACCESS_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:
field is set instead:
Lists that need at least one item
Since October 27, 2026, the v2 buyer API follows AdCP 3.3 for many list fields: a list that is present needs at least one item, and an empty list is refused withVALIDATION_ERROR. To say “none”, leave the field out instead of sending [].
These v2 buyer requests carry such a list:
Reads do not take this rule: a response returns what is stored, so a stored empty list still comes back as an empty list.
Handling errors in client code
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 storefront’s AdCP update_media_buy on a Meta media buy, when it is sent straight to Meta rather than held for seller approval, returns CONFLICT if the request’s revision no longer matches the buy. details.mutation_outcome is not_dispatched: nothing in the request was applied on Meta. MCP classifies it as transient in the AdCP sense: re-read the buy with get_media_buys, check its current state, and resubmit with the returned revision. Resending the identical request unchanged fails again.