Errors

The error envelope and every code you can branch on.

Every failure returns the same shape with a matching HTTP status:

{
  "error": {
    "code": "validation_failed",
    "message": "Keyword bids are rejected on automated-bidding campaigns.",
    "validation_errors": ["Campaign 123 uses MAX_CONVERSIONS; keyword bids are ignored."],
    "action_id": "9f2c…"
  }
}

Branch on code. It is stable. message is for humans and may change between releases.

Codes

Authentication and addressing

CodeStatusMeaning
unauthorized401Missing, invalid, revoked or expired key. Deliberately undifferentiated — the API never confirms a key existed.
forbidden_scope403The key's scope is too low, or an account-scoped key hit an org-level endpoint. Carries required_scope and key_scope.
account_mismatch403The key can't reach that projectId.
account_not_found404No Apple Ads account is connected to that project.
not_found404The campaign, ad group, keyword, ad or action doesn't exist in this account.

Request

CodeStatusMeaning
invalid_request400Malformed body, bad enum, missing required field. The message names the field.
validation_failed422The request was well-formed; the account state rejected it.

validation_failed is a 422 rather than a 400 on purpose. Apple has no validate_only endpoint and no sandbox, so we validate locally at propose time — and a reject is a fact about your account, not about your JSON. It carries validation_errors (every reason) and action_id (the auditable failed row we recorded, so the reject stays in your ledger).

Account state

CodeStatusMeaning
no_op409The change is already true — the campaign already targets those storefronts, Search Match is already on, the keyword is already EXACT.
bid_strategy_conflict422A keyword or default bid on a MAX_CONVERSIONS campaign. Apple ignores manual bids under automated bidding, so we reject rather than pretend.
conflict409The action is no longer pending, or its account is gone.
not_revertible422Only applied actions can be undone, it was already undone, or this kind has no compensating change.
sync_in_progress409A sync is already running for this account.
connection_error409No Apple Ads credentials, or they've stopped working. A human must fix this in the dashboard.
read_only_account403The credential's ACL lacks the API Account Manager role for this org, so writes can't work.

Upstream and infrastructure

CodeStatusMeaning
apple_error502Apple's API returned an error. Carries request_id when Apple gave one.
apple_unavailable503Apple is rate-limiting or temporarily down. Retryable — carries retry_after_ms and Retry-After.
rate_limited429Our own limiter. Carries retry_after_ms and Retry-After.
internal500Our bug. The message is deliberately generic.

What is not an error

Three cases that look like failures and aren't. Handling them as errors is the most common way to get this API wrong — see Policy states for the full picture.

  1. mode: "queued" on a proposal. The kill switch, an inactive account, or the daily auto-apply cap all force review. The proposal succeeded; it's waiting for a human.
  2. A demoted action. mode: "auto_applied" can still become proposed when the apply worker re-checks the policy. Poll the ledger.
  3. An applied action with warnings. Apple's bulk endpoints can return 200 with per-row errors, so "applied" doesn't always mean every row landed. Read warnings.

Retrying

apple_unavailable (503) and rate_limited (429) are safe to retry after retry_after_ms. apple_error (502) is not — the call may have partially written, and Apple gives us no way to tell.

For proposals, always send an Idempotency-Key. A retry then replays the original action rather than proposing a second money-moving change:

curl -X POST .../api/v1/accounts/current/actions/budget-change \
  -H "Authorization: Bearer tsk_…" \
  -H "Idempotency-Key: nightly-budget-2026-07-26" \
  -H "Content-Type: application/json" \
  -d '{"campaignId":"123","newDailyBudgetMicros":150000000,"reason":"CPA is inside band and the campaign is budget-capped."}'

A replay returns 200 with Idempotency-Replay: true and replayed: true in the body, instead of the original 201.

On this page