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
| Code | Status | Meaning |
|---|---|---|
unauthorized | 401 | Missing, invalid, revoked or expired key. Deliberately undifferentiated — the API never confirms a key existed. |
forbidden_scope | 403 | The key's scope is too low, or an account-scoped key hit an org-level endpoint. Carries required_scope and key_scope. |
account_mismatch | 403 | The key can't reach that projectId. |
account_not_found | 404 | No Apple Ads account is connected to that project. |
not_found | 404 | The campaign, ad group, keyword, ad or action doesn't exist in this account. |
Request
| Code | Status | Meaning |
|---|---|---|
invalid_request | 400 | Malformed body, bad enum, missing required field. The message names the field. |
validation_failed | 422 | The 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
| Code | Status | Meaning |
|---|---|---|
no_op | 409 | The change is already true — the campaign already targets those storefronts, Search Match is already on, the keyword is already EXACT. |
bid_strategy_conflict | 422 | A keyword or default bid on a MAX_CONVERSIONS campaign. Apple ignores manual bids under automated bidding, so we reject rather than pretend. |
conflict | 409 | The action is no longer pending, or its account is gone. |
not_revertible | 422 | Only applied actions can be undone, it was already undone, or this kind has no compensating change. |
sync_in_progress | 409 | A sync is already running for this account. |
connection_error | 409 | No Apple Ads credentials, or they've stopped working. A human must fix this in the dashboard. |
read_only_account | 403 | The credential's ACL lacks the API Account Manager role for this org, so writes can't work. |
Upstream and infrastructure
| Code | Status | Meaning |
|---|---|---|
apple_error | 502 | Apple's API returned an error. Carries request_id when Apple gave one. |
apple_unavailable | 503 | Apple is rate-limiting or temporarily down. Retryable — carries retry_after_ms and Retry-After. |
rate_limited | 429 | Our own limiter. Carries retry_after_ms and Retry-After. |
internal | 500 | Our 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.
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.- A demoted action.
mode: "auto_applied"can still becomeproposedwhen the apply worker re-checks the policy. Poll the ledger. - An
appliedaction withwarnings. Apple's bulk endpoints can return 200 with per-row errors, so "applied" doesn't always mean every row landed. Readwarnings.
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.