> ## Documentation Index
> Fetch the complete documentation index at: https://agenticadvertisingorg-snap-format-preview-links.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Error Handling

> AdCP error handling: protocol errors, task failures, and validation errors with standard error codes, recovery strategies, and exponential backoff retry logic.

AdCP uses a consistent error handling approach across all operations. Understanding error categories and implementing proper recovery strategies is essential for building robust integrations.

## Compliance Levels

Sellers can adopt error handling incrementally. Each level builds on the previous:

| Level       | What to implement                                                                                                                           | What agents can do                                                   |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| **Level 1** | Return `code` + `message` on every error                                                                                                    | Agents match on error code to classify failures                      |
| **Level 2** | Add `recovery`, `retry_after`, `field`, and `suggestion`                                                                                    | Agents auto-retry transient errors and self-correct correctable ones |
| **Level 3** | Use [transport bindings](/docs/building/operating/transport-errors) to put errors in `structuredContent` (MCP) or artifact `DataPart` (A2A) | Programmatic clients get typed errors without parsing text           |

**Level 1** is the minimum for a conformant implementation. **Level 2** is where agent-driven recovery becomes possible — without `recovery`, agents must guess from the error code. **Level 3** is where client libraries like `@adcp/sdk` can provide fully typed error objects.

## Error Categories

### 1. Protocol Errors

Transport/connection issues not related to AdCP business logic:

* Network timeouts
* Connection refused
* TLS/SSL errors
* JSON parsing errors

**Handling:** Retry with exponential backoff.

### 2. Task Errors

Business logic failures returned as `status: "failed"`:

* Insufficient inventory
* Invalid targeting
* Budget validation failures
* Resource not found

**Handling:** Check the `recovery` field to determine whether to retry, fix the request, or escalate.

### 3. Validation Errors

Malformed requests that fail schema validation:

* Missing required fields
* Invalid field types
* Out-of-range values

**Handling:** Fix request format and retry. Usually development-time issues.

## Error Response Format

Failed operations return status `failed` with error details. The error object follows the [`error.json`](https://adcontextprotocol.org/schemas/v3/core/error.json) schema:

```json theme={null}
{
  "status": "failed",
  "message": "Budget is below the seller's minimum for this product",
  "errors": [
    {
      "code": "BUDGET_TOO_LOW",
      "message": "Budget is below the seller's minimum for this product",
      "recovery": "correctable",
      "field": "budget.total",
      "suggestion": "Increase budget to at least 500 USD",
      "details": {
        "minimum_budget": 500,
        "currency": "USD"
      }
    }
  ]
}
```

### Envelope vs. payload errors — the two-layer model

AdCP exposes errors in two distinct places, and implementers need to populate the right layer for the right situation. This is the single most common source of error-shape drift between agents and storyboards.

| Layer                  | Key                                                                  | When to populate                                                          | Shape                                                                                               |
| ---------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| **Task payload**       | `payload.errors[]` (or top-level `errors[]`, depending on transport) | The task ran; the payload reports one or more issues (fatal or non-fatal) | Array of error objects per [`error.json`](https://adcontextprotocol.org/schemas/v3/core/error.json) |
| **Transport envelope** | `adcp_error`                                                         | The task failed and the transport needs a typed, extractable signal       | Single error object per [`error.json`](https://adcontextprotocol.org/schemas/v3/core/error.json)    |

**A fatal task failure SHOULD populate both layers.** The payload carries the structured `errors[]` array that any protocol can read verbatim, and the transport envelope carries `adcp_error` so MCP/A2A clients can extract a typed error without re-parsing the payload. Populating only one of the two is the source-of-truth for most interop bugs — a runner that reads the transport envelope sees no error, and a runner that reads the payload sees no error signal on the transport:

```json theme={null}
// MCP — structuredContent AND payload both carry the error
{
  "content": [{"type": "text", "text": "{\"adcp_error\":{\"code\":\"BUDGET_TOO_LOW\", ...}}"}],
  "isError": true,
  "structuredContent": {
    "adcp_error": { "code": "BUDGET_TOO_LOW", "message": "...", "recovery": "correctable" },
    "payload": {
      "errors": [
        { "code": "BUDGET_TOO_LOW", "message": "...", "recovery": "correctable", "field": "budget.total" }
      ]
    }
  }
}
```

```json theme={null}
// A2A — artifact DataPart carries adcp_error; if the agent also surfaces payload via a sibling DataPart, errors[] lives there
{
  "status": { "state": "failed" },
  "artifacts": [{
    "artifactId": "error-result",
    "parts": [
      { "kind": "data", "data": { "adcp_error": { "code": "BUDGET_TOO_LOW", ... } } },
      { "kind": "data", "data": { "errors": [{ "code": "BUDGET_TOO_LOW", "field": "budget.total", ... }] } }
    ]
  }]
}
```

**Non-fatal errors populate only the payload.** A `status: "submitted"` or `status: "input-required"` task reporting a warning (e.g., "this media buy requires manual approval — \[warning details]") populates `errors[]` in the payload with `severity: "warning"` but MUST NOT populate `adcp_error`. The transport envelope signals "the task failed" — it is not a warning channel.

**Storyboard validators.** Prefer `check: error_code` over `check: field_present, path: "errors"` when asserting on a failed task's error code. `error_code` is shape-agnostic — the runner resolves it from either `adcp_error.code` (transport) or `errors[0].code` (payload). Direct `path: "errors"` checks pin the assertion to the payload shape and fail against agents that surface errors only via the transport envelope, even when the agent is conformant. See [Storyboard authoring — Asserting on errors](/docs/contributing/storyboard-authoring#asserting-on-errors).

**Discriminated rejection arms.** When the task response defines a structured rejection arm (e.g., `AcquireRightsRejected`, `CreativeRejected` — see the wire-placement guidance on [`GOVERNANCE_DENIED`](https://adcontextprotocol.org/schemas/v3/enums/error-code.json) for the rule), the spec-correct denial response carries no error code on the wire — the rejection arm enforces `not: { required: [errors] }` at the schema layer. Asserting `check: error_code` will fail against a conformant agent. Assert on the discriminator instead: `check: field_value, path: "status", value: "rejected"`. This is the pattern for governance denial on `acquire_rights` and policy denial on `creative_approval`; assertions that mix the two paths (`error_code` for tasks with rejection arms, `field_value` for tasks without) bake non-spec opinions into the storyboard.

### Error Object Fields

These fields are defined by the [`error.json`](https://adcontextprotocol.org/schemas/v3/core/error.json) schema:

| Field         | Type   | Required | Description                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| ------------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `code`        | string | Yes      | Machine-readable error code from the [standard vocabulary](#standard-error-codes) or a seller-specific code                                                                                                                                                                                                                                                                                                                                               |
| `message`     | string | Yes      | Human-readable error description                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `recovery`    | string | No       | Agent recovery classification: `transient`, `correctable`, or `terminal`                                                                                                                                                                                                                                                                                                                                                                                  |
| `retry_after` | number | No       | Seconds to wait before retrying (transient errors)                                                                                                                                                                                                                                                                                                                                                                                                        |
| `field`       | string | No       | Field path in JSONPath-lite format (e.g., `packages[0].targeting`). When `issues` is present, sellers MUST set this to `issues[0].pointer` translated from RFC 6901 to JSONPath-lite (e.g., `/packages/0/targeting` → `packages[0].targeting`). Will be deprecated in a future major version.                                                                                                                                                             |
| `issues`      | array  | No       | Structured list of validation failures. Each entry carries `pointer` (RFC 6901), `message`, `keyword` (JSON Schema keyword that rejected — `required` / `type` / `format` / etc.), and optionally `schema_id`, `schemaPath`, `discriminator`. See [Validator-internals fields](#validator-internals-fields-on-issues) for the `schema_id` / `schemaPath` / `discriminator` semantics, the production-emit rules, and the resolution path for `schema_id`. |
| `suggestion`  | string | No       | Suggested fix for the error                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `details`     | object | No       | Additional context-specific information. Sellers MAY mirror `issues[]` here as `details.issues` for backward compatibility with pre-3.1 consumers; new consumers SHOULD prefer the top-level `issues` field.                                                                                                                                                                                                                                              |

### Validator-internals fields on `issues`

Three optional fields on each `issues[]` entry name the schema element that rejected the payload, so agents can recover from validation errors in one iteration instead of probing variants:

| Field           | Shape                                                                         | Purpose                                                                                                                                                                     |
| --------------- | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `schema_id`     | string — a published `$id` (e.g. `/schemas/3.1.0/core/activation-key.json`)   | Canonical name of the rejecting (sub-)schema. 3.1+ consumers' primary handle.                                                                                               |
| `schemaPath`    | string — a JSON Schema tree path (e.g. `#/properties/packages/items/oneOf/1`) | Validator-internal traversal. Retained for 3.0.x backward compatibility; 3.1+ consumers SHOULD prefer `schema_id`. (Renamed to `schema_path` in a future major.)            |
| `discriminator` | array of `{property_name, value}`                                             | Variant the validator selected for const-discriminated `oneOf` / `anyOf`, sourced from values present in the payload. Aligns with OpenAPI 3.x `discriminator.propertyName`. |

**Resolving `schema_id`.** The string is the schema's `$id`. To load the schema:

* **HTTPS canonical:** prepend `https://adcontextprotocol.org` (e.g. `https://adcontextprotocol.org/schemas/3.1.0/core/activation-key.json`). Cacheable; immutable per version.
* **SDK-bundled:** `@adcp/sdk` and `adcp-client-python` ship the schema bundle for offline resolution — look up by `$id` against the bundled tree.
* **Bundled-tree caveat.** Tools served from the pre-resolved bundled tree (`/schemas/{version}/bundled/...`) inline sub-schemas with their `$id` preserved (3.1+, see #3868) — but only on the **first occurrence** of each sub-schema within a bundle. Same source schema referenced from multiple co-locations gets `$id` only on the first inline; subsequent occurrences fall back to the nearest `$id`-bearing ancestor (typically the response root) when SDK error reporting walks up the schema tree. Sub-schemas whose subtrees contain hoisted `$defs` references also have `$id` stripped at bundle time, because preserving `$id` there would break local-fragment resolution. Consumers reading bundles produced before #3868 see only the response-root `$id`. Detect the pre-#3868 case by checking whether the `$id` ends in `/bundled/<tool>-response.json` — if so, fall back to walking the bundled schema by `pointer`.

**Discriminator semantics.** Sellers populate `discriminator` only when (a) the rejecting schema is a const-discriminated `oneOf` / `anyOf`, and (b) the discriminator property is present in the payload. The wire field reports the value the caller sent — not a validator inference on partial-match heuristics — so the field is deterministic across Ajv, Python `jsonschema`, and `gojsonschema`. When zero variants survive validation, sellers MUST omit `discriminator` (omission is the signal to the agent that "the validator could not localize a target variant"). For compound discriminators (e.g. `audience-selector`'s `(type, value_type)`), entries are ordered by declaration in the rejecting schema's `properties` block.

When the discriminator property is *missing* from the payload (the validator can't even start branch selection), sellers omit `discriminator` — the recovery signal comes from a sibling `issues[]` entry with `keyword: "required"` whose `pointer` names the absent discriminator property. Agents reading "missing required discriminator field" + "no `discriminator` on this issue" recover by populating the named property; agents seeing a `discriminator` array with a value that didn't match any branch recover by switching to a different value.

**Production-emit rules (the public-spec stance).** All three fields are emit-on-production safe **when the rejecting element is in the published spec at the version the seller advertises via `get_adcp_capabilities`**. The rationale is replay-locally: schemas are published at adcontextprotocol.org and bundled with every SDK, so an adversary running the same validator against the same payload derives the same branch selection — the wire field carries no information they can't compute.

The replay-locally argument has carve-outs sellers MUST honor:

* **Private extensions.** Sellers running schemas with custom `oneOf` branches, server-only sub-schemas, or enum subsets layered via `additionalProperties: true` MUST NOT emit `schema_id`, `schemaPath`, or `discriminator` when the rejecting element is not present in the published spec. Emission would leak seller-internal validation state the adversary cannot replay. *Implementation:* sellers running a mixed public + private validation tree typically (a) compile public-only and private-only validators separately and emit `schema_id` only from the public run, or (b) instrument compiled schemas with a side-table mapping each `$id` to its source bundle.
* **Version skew.** Sellers validating against a pre-release or post-release schema MUST NOT emit a `schema_id` whose `$id` is not present in the published bundle for the version named in `get_adcp_capabilities`.
* **Server-narrowed public elements.** When the seller server-side filters a public enum, pattern, or numeric range to a tenant-specific subset (e.g. accepts `["a","b","c"]` per the public schema, rejects everything except `["a"]` for this caller), sellers MUST NOT return `VALIDATION_ERROR` with `keyword: "enum"` (or `pattern` / `minimum` / `maximum`) against the public `schema_id`. The public-spec replay would accept the value; the seller rejects on private state. Use `POLICY_VIOLATION` or `UNSUPPORTED_FEATURE` instead so the rejection isn't mis-attributed to a public schema element. The structural delta between "public-replay accepts, seller rejects" is itself a fingerprint of the seller's private subset.
* **Custom keywords.** `keyword` MUST be drawn from the JSON Schema Draft 7 / 2020-12 vocabulary — sellers using validator-specific custom keywords (Ajv `addKeyword`, `instanceof`) MUST NOT emit them on the wire.
* **Probe terseness.** Sellers MAY scope these three fields to dev/sandbox responses on rate-limited endpoints to keep production envelopes terse, even when the carve-outs above don't apply. Field omission is always conformant.

## Standard Error Codes

Standard error codes are defined in [`error-code.json`](https://adcontextprotocol.org/schemas/v3/enums/error-code.json). The vocabulary is **open**: `error.code` is wire-typed as `string`, the standard codes are documentary, and senders MAY emit codes outside the standard set.

### Forward-compatible decoding (normative)

**The error-code vocabulary is open.** `error.code` is typed as `string` in [`core/error.json`](https://adcontextprotocol.org/schemas/v3/core/error.json) — not as a closed enum — so a strict JSON Schema validator MUST accept any string value. The standard vocabulary in `error-code.json` is documentary; it constrains neither sender nor receiver at the wire level.

**Receivers MUST decode unknown codes.** A receiver pinned to AdCP version X that decodes a response carrying an `error.code` introduced in version X+1 (or a platform-specific code outside the standard vocabulary) MUST:

1. Treat the response as well-formed — MUST NOT reject the envelope, throw a deserialization exception, or downgrade to a generic protocol error.
2. Recover the recovery classification from `error.recovery` (top-level field on the error envelope) when present. `error.recovery` is the normative carrier; the `enumMetadata.recovery` in `error-code.json` is the documentary mirror.
3. When `error.recovery` is absent (legacy senders), apply a conservative default. `transient` is the safe default for unknown codes — retry-with-backoff cannot do worse than terminal classification, and the manifest's [`error_code_policy.default_unknown_recovery`](https://adcontextprotocol.org/schemas/v3/manifest.schema.json) documents this as the canonical fallback. The `transient` default is **bounded** by the retry rules in [§ Retry Logic](#retry-logic) — receivers MUST apply `maxRetries` and the jittered exponential-backoff schedule, and MUST NOT loop indefinitely on a `transient` default. A hostile or buggy sender cannot induce unbounded retries against a conformant client; the open-enum decoding rule does not exempt receivers from the retry budget.

**Senders MAY emit codes outside the receiver's pinned vocabulary.** A sender emitting a 3.1-era code (e.g., `PROPOSAL_NOT_FOUND`) to a 3.0-pinned receiver does not violate the spec — the receiver is required by the rule above to handle it. When a sender knows the receiver's pinned version (via `adcp_version` envelope echo or capability discovery), senders SHOULD prefer a code from that version's vocabulary when an equivalent exists; senders MAY emit newer or platform-specific codes when no equivalent is available.

**Senders MUST populate `error.recovery` on every error.** This is the normative carrier of recovery semantics across version skew — receivers cannot reliably classify a code they don't know, but they can always read `error.recovery`. Omitting it defeats the forward-compat rule.

**Why this matters.** Forward-compatible decoding is the wire-level invariant that lets future maintenance lines (3.1.x, 4.0.x, …) ship new error codes additively without breaking older receivers. Without it, every new code is a wire change held to the next minor — the current drift-lint policy on 3.0.x. With it in 3.1+, new codes can register during a maintenance line's lifetime, which matters as adopters' real-world rejection paths surface new codes.

**3.0.x policy unchanged.** 3.0.x receivers predate this normative rule, so 3.0.x stays wire-stable for the remainder of its support window — new codes still land at the next minor. This rule sets the receiver contract from 3.1 onward.

**Not-found precedence.** When a referenced identifier does not resolve, sellers SHOULD return the resource-specific code when the resolved type is known from the request: `PRODUCT_NOT_FOUND` for `product_id`, `PACKAGE_NOT_FOUND` for `package_id`, `MEDIA_BUY_NOT_FOUND` for `media_buy_id`, `CREATIVE_NOT_FOUND` for `creative_id`, `SIGNAL_NOT_FOUND` for `signal_id`, `SESSION_NOT_FOUND` for SI `session_id`, `ACCOUNT_NOT_FOUND` for `account_id`, `PLAN_NOT_FOUND` for governance `plan_id`. Fall back to `REFERENCE_NOT_FOUND` for resource types without a dedicated code (e.g., property lists, content standards, rights grants, SI offerings, proposals, catalogs, event sources, collection lists, brands, individual properties). Typed parameters that lack a dedicated standard code MUST use `REFERENCE_NOT_FOUND` rather than minting a custom `*_NOT_FOUND` code — the vocabulary grows by upstream spec change, not by per-seller inflation. Clients SHOULD switch on `error.code` first; the resource-specific codes let clients dispatch without parsing `error.field`.

**Polymorphic parameters.** When the unresolved identifier was supplied via a polymorphic or untyped parameter (a field that accepts multiple resource types), sellers MUST use `REFERENCE_NOT_FOUND` even if a resource-specific code exists for the resolved type. Using the type-specific code on a polymorphic parameter leaks the resolved type to an unauthorized caller. Polymorphism is evaluated against the parameter's declared shape in the tool schema — **before any lookup** — so a generic `reference_id` parameter dispatches to `REFERENCE_NOT_FOUND` regardless of what the id resolves to. Evaluating on the resolved type after dispatch reintroduces the leak.

A tool's declared parameter shape MUST be identical across all callers for a given tool version; dispatch rules MUST NOT be conditioned on caller identity. A schema that exposes `property_list_id` to tenant B but only `reference_id` to tenant A turns capability discovery itself into an enumeration oracle (read the schema under two identities, diff).

**Uniform response for inaccessible references.** The uniform-response requirement applies to **every** not-found code in this vocabulary (`REFERENCE_NOT_FOUND`, `SIGNAL_NOT_FOUND`, `CREATIVE_NOT_FOUND`, `MEDIA_BUY_NOT_FOUND`, `PACKAGE_NOT_FOUND`, `SESSION_NOT_FOUND`, `ACCOUNT_NOT_FOUND`, `PLAN_NOT_FOUND`): sellers MUST return the same response for "exists but the caller lacks access" as for "does not exist". Never distinguish the two — this is how cross-tenant enumeration lands.

The MUST covers every observable channel, not just `error.code`:

* **Error object.** `error.code`, `error.message`, `error.field`, `error.details` MUST be byte-equivalent between the two cases. On typed parameters that fall back to `REFERENCE_NOT_FOUND`, `error.field` MUST be identical across true-miss and resolve-then-deny — either omitted on both or replaced with a type-neutral name on both. `error.field` MAY name the input parameter when the parameter name is type-neutral (e.g., `reference_id`); when the original parameter name is type-revealing (e.g., `property_list_id`), `error.field` MUST be omitted or replaced with a neutral name. `error.message` MUST be generic (no resource-qualified text like `"Property list not found"`). For `REFERENCE_NOT_FOUND` specifically, sellers MUST NOT leak the resolved resource type via `error.field`, `error.details`, or a resource-qualified `error.message`. When the parameter is an array (e.g., `catalog_ids`, `format_ids`), `error.field` MUST name the array parameter itself. Sellers MAY enumerate specific unresolvable elements in `error.details` — but only when the elements were supplied verbatim by the caller. Sellers MUST NOT distinguish "supplied element resolved but caller unauthorized" from "supplied element does not exist" at the element level; that reintroduces the enumeration oracle at the array-entry granularity.
* **Transport status.** HTTP status code, A2A `task.status.state`, and MCP `isError` MUST be identical between the two cases.
* **Response headers.** `ETag`, `Cache-Control`, per-resource-type rate-limit buckets, CDN tags, and any header whose value or presence differs by resource type MUST be identical.
* **Side effects.** Webhook dispatch and audit-log writes MUST be identical — a resolve-then-deny path MUST NOT write tenant audit rows, enqueue background work (search-indexer updates, cache warmers, access-log aggregators), increment per-resource-type quota/rate-limit counters, or fire webhooks to any subscriber (including the resource owner) in ways a true-miss would not. If the resolve-then-deny path touches a per-tenant DB shard or cache, the true-miss path MUST touch the same shape of storage (e.g., route both through a tenant-agnostic resolver) so that co-tenant observers cannot distinguish via storage-layer metrics.
* **Observability.** Downstream logs, APM spans, and third-party error-reporting telemetry (Sentry, Datadog, Rollbar, and equivalents) MUST NOT be tagged with the resolved resource type when the caller lacks access; the trace a true-miss emits MUST be structurally indistinguishable from the trace a resolve-then-deny emits.

To make latency parity a consequence rather than a separate requirement, sellers MUST perform the same resolution-and-authorization work on both paths — **resolve-then-authorize**, never short-circuit on "unknown id." On a true-miss, sellers MUST still execute an authorization decision of equivalent shape (e.g., against an empty principal set, or against the caller's own tenant as a decoy) so that authorizer latency — which varies with the size and shape of the ACL graph — is not a side channel. Pre-lookup input validation (UUID format, length, regex) is permitted iff it is deterministic in request content only (same input → same verdict, regardless of caller or existence).

Non-normative implementation note: a single-query pattern like `SELECT ... WHERE id = ? AND tenant = ?` *looks* uniform but differs in execution plan, buffer-pool touches, and authorizer invocation depending on whether the row exists in another tenant. Prefer the two-step pattern — resolve by id, then authorize against the loaded row (or against an empty row on true-miss) — as the only pattern that naturally produces observational uniformity.

**Cache warmth** is a distinct oracle: a warm cache on tenant B's id indicates someone accessed it recently. Sellers MUST NOT gate cache population on authorization — true-miss ids MUST be cached-as-miss with the same TTL as resolve-then-deny, or cache reads MUST be bypassed for not-found responses.

**Verifying this yourself.** The paired-probe `adcp fuzz` invariant checks uniform-response compliance by comparing two responses per tool. See [Validate Your Agent — Preparing to test uniform error responses](/docs/building/verification/validate-your-agent#preparing-to-test-uniform-error-responses) for tenant-setup requirements and CLI invocation. Full-strength testing requires two isolated tenants; single-tenant runs cover only the "does not exist" leg.

### Authentication and Access

| Code                       | Recovery      | Description                                                                                                                                                                                                                                                                                                                                                          | Resolution                                                                                                                                                                                                                                                                |
| -------------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AUTH_REQUIRED`            | correctable\* | Authentication is required, or presented credentials were rejected                                                                                                                                                                                                                                                                                                   | Provide credentials when missing; escalate to operator when rejected — see the warning below                                                                                                                                                                              |
| `CREDENTIAL_IN_ARGS`       | terminal      | Authentication material or caller-supplied trust material was placed in request args (top-level, `context`, `ext`, or any nested location) instead of the relevant transport authentication or trust channel. This includes inbound buyer-principal credentials and evaluator-call credentials or JWK/JWKS/JWKS-URI material smuggled into evaluator payload fields. | Do NOT auto-retry — auto-retry re-logs the credential. Move the credential onto the relevant transport authentication/trust channel ([Credential placement](/docs/building/by-layer/L2/authentication#credential-placement)), rotate any leaked credential, then resubmit |
| `ACCOUNT_NOT_FOUND`        | terminal      | Account reference could not be resolved                                                                                                                                                                                                                                                                                                                              | Verify via `list_accounts` or contact seller                                                                                                                                                                                                                              |
| `ACCOUNT_SETUP_REQUIRED`   | correctable   | Account needs setup before use                                                                                                                                                                                                                                                                                                                                       | Check `details.setup` for URL or instructions                                                                                                                                                                                                                             |
| `ACCOUNT_AMBIGUOUS`        | correctable   | Natural key resolves to multiple accounts                                                                                                                                                                                                                                                                                                                            | Pass explicit `account_id` or a more specific natural key                                                                                                                                                                                                                 |
| `ACCOUNT_PAYMENT_REQUIRED` | terminal      | Outstanding balance requires payment                                                                                                                                                                                                                                                                                                                                 | Buyer must resolve billing                                                                                                                                                                                                                                                |
| `ACCOUNT_SUSPENDED`        | terminal      | Account has been suspended                                                                                                                                                                                                                                                                                                                                           | Contact seller to resolve                                                                                                                                                                                                                                                 |

<Warning>
  **`AUTH_REQUIRED` sub-cases — do not auto-retry rejected credentials.** The wire code carries two operationally distinct cases that agents MUST handle differently:

  * **Credentials missing** → provide credentials and retry once. Correctable inside the agent loop.
  * **Credentials presented but rejected** (expired, revoked, or malformed signature) → do **not** auto-retry; escalate to operator for credential rotation. Re-presenting a rejected credential against an SSO endpoint creates retry-storm patterns indistinguishable from brute-force probes — the seller's fraud detection may rate-limit, suspend, or alert on the calling agent.

  `CREDENTIAL_IN_ARGS` is the related — but distinct — case where authentication material or caller-supplied trust material was placed in the **task payload** instead of the relevant transport authentication or trust channel. That code is `terminal` (auto-retry re-logs the credential) and the rule + carve-outs (push-notification webhook auth, relay topology) live in [Credential placement](/docs/building/by-layer/L2/authentication#credential-placement).

  A future minor release splits this code into `AUTH_MISSING` (correctable) and `AUTH_INVALID` (terminal). Until then, agents branch on whether credentials were attached to the failing request:

  ```javascript theme={null}
  case 'AUTH_REQUIRED': {
    // The caller's request builder records whether an auth header was attached.
    // The error-handling SDK surfaces this on `error.request_had_credentials` (or you
    // pass it in from your own request wrapper).
    const requestHadCredentials = Boolean(error.request_had_credentials);
    if (!requestHadCredentials) {
      // Sub-case (a) — provide credentials and retry.
      await refreshCredentials();
      return retry();
    }
    // Sub-case (b) — credentials were presented and rejected.
    // Treat as terminal at the application layer; surface to operator.
    console.error('Credential rejected — needs human rotation:', error.message);
    throw error;
  }
  ```
</Warning>

### Billing and Account Setup

Returned by [`sync_accounts`](/docs/accounts/tasks/sync_accounts) when the request's billing or account-shape values are not acceptable to the seller. The two billing-rejection codes distinguish *which gate* fired so agents can dispatch on the right recovery — autonomous retry vs surface-to-human — without parsing prose. See [Buyer-agent identity](/docs/building/by-layer/L2/accounts-and-agents#buyer-agent-identity) for the two-layer identity model these codes sit on top of.

| Code                              | Recovery    | Description                                                                                                                                                                                                                                                                                                    | Resolution                                                                                                                                                                                                                                                                                                       |
| --------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `BILLING_NOT_SUPPORTED`           | correctable | Seller declines the requested `billing` value at the seller-wide capability level (`supported_billing` does not include the value) or at the per-account-relationship level (e.g., the seller accepts `operator` billing in general but has no direct relationship with the operator on this specific account) | Check `get_adcp_capabilities` for `supported_billing`, resubmit with a supported value, or omit `billing`; when present, dispatch on `error.details.scope` (`"capability"` or `"account"`) per [`billing-not-supported.json`](https://adcontextprotocol.org/schemas/v3/error-details/billing-not-supported.json) |
| `BILLING_NOT_PERMITTED_FOR_AGENT` | correctable | Seller-wide capability accepts the requested value, but the calling buyer agent's commercial relationship does not (e.g., onboarded as passthrough-only — no payments relationship — so only `operator` billing is permitted)                                                                                  | Retry with `error.details.suggested_billing` (typically `operator`) when present; when absent, the rejection is terminal-pending-onboarding — the agent MUST NOT auto-retry and MUST surface to a human at the buyer to complete payments-relationship onboarding with the seller offline                        |
| `PAYMENT_TERMS_NOT_SUPPORTED`     | correctable | Seller does not accept the requested `payment_terms` value                                                                                                                                                                                                                                                     | Omit `payment_terms` to accept the default, retry with a different supported value, or negotiate offline                                                                                                                                                                                                         |
| `BRAND_REQUIRED`                  | correctable | Billable operation attempted without a brand reference                                                                                                                                                                                                                                                         | Include `brand` (`domain` plus optional `brand_id`) on the request                                                                                                                                                                                                                                               |

Normative requirements:

* **Uniform response without established agent identity.** `BILLING_NOT_PERMITTED_FOR_AGENT` differs from `BILLING_NOT_SUPPORTED` based on the caller's onboarded commercial state with the seller. Returning the per-agent code without established identity lets unauthenticated probes use the code-choice as an oracle for "is this agent onboarded as agent-billable?" — same shape as the [`*_NOT_FOUND` uniform-response rule](#standard-error-codes). The bright line: sellers MUST emit `BILLING_NOT_PERMITTED_FOR_AGENT` only when agent identity has been established via signed-request derivation per [Agent identity](/docs/building/by-layer/L1/security#agent-identity) or via a credential-to-agent mapping in the seller's onboarding record. In all other cases — including bearer credentials not mapped to a specific agent record — sellers MUST return `BILLING_NOT_SUPPORTED`, and `error.details.scope` MUST be omitted on this path so a `"account"` scope hint cannot itself act as a per-account-relationship oracle.
* **`BILLING_NOT_PERMITTED_FOR_AGENT` details shape is clamped.** `error.details` MUST conform to [`error-details/billing-not-permitted-for-agent.json`](https://adcontextprotocol.org/schemas/v3/error-details/billing-not-permitted-for-agent.json): `rejected_billing` (echoed) plus an optional single `suggested_billing` retry value. The schema sets `additionalProperties: false`. The shape MUST NOT carry the agent's full permitted-billing subset, rate cards, payment terms, credit limit, billing entity, or any other per-agent commercial state — full-subset disclosure in a single probe is exactly the oracle the clamp prevents.
* **One-shot retry.** A buyer agent that retries with `error.details.suggested_billing` and receives a second `BILLING_NOT_PERMITTED_FOR_AGENT` MUST surface to a human rather than retrying again. Recovery is bounded to a single seller-suggested fallback; further iteration indicates seller misconfiguration or onboarding state the agent cannot resolve autonomously.

**Recovery dispatch — example.** The two billing codes recover differently. Implementers SHOULD branch explicitly rather than collapsing to a single retry path. The snippet below uses the response shape returned directly by the seller (the `accounts[].errors[]` array on a `sync_accounts` response — see [task reference](/docs/accounts/tasks/sync_accounts)), not the `@adcp/sdk/testing` wrapper used elsewhere in task examples.

```javascript theme={null}
async function syncAccountsWithRecovery(client, account) {
  const result = await client.syncAccounts({ accounts: [account] });
  const error = result.accounts[0]?.errors?.[0];
  if (!error) return result;

  switch (error.code) {
    case 'BILLING_NOT_SUPPORTED': {
      // Seller-wide or per-account gate. Check capabilities, dispatch on scope.
      const scope = error.details?.scope;  // "capability" | "account"
      if (scope === 'capability') {
        // The seller never accepts this value. Pick from supported_billing.
        const supported = error.details?.supported_billing ?? [];
        if (supported.length === 0) return surfaceToHuman(error);
        return client.syncAccounts({
          accounts: [{ ...account, billing: supported[0] }],
        });
      }
      // Per-account-relationship reject — the operator-on-this-account isn't
      // billable directly. Try the next-most-permissive value the seller's
      // capability allows.
      return tryNextBillingValue(client, account, error);
    }

    case 'BILLING_NOT_PERMITTED_FOR_AGENT': {
      // Per-buyer-agent commercial gate. Autonomous retry only when the seller
      // suggests a fallback; otherwise surface — the agent cannot extend its
      // own commercial relationship.
      const suggested = error.details?.suggested_billing;
      if (!suggested) return surfaceToHuman(error);
      return client.syncAccounts({
        accounts: [{ ...account, billing: suggested }],
      });
    }

    default:
      throw error;
  }
}
```

**Example envelope** for `BILLING_NOT_PERMITTED_FOR_AGENT` — passthrough-only buyer agent receives a fallback to `operator`:

```json theme={null}
{
  "accounts": [{
    "brand": { "domain": "nova-brands.com", "brand_id": "spark" },
    "operator": "pinnacle-media.com",
    "action": "failed",
    "status": "rejected",
    "errors": [{
      "code": "BILLING_NOT_PERMITTED_FOR_AGENT",
      "message": "This buyer agent is onboarded as passthrough-only; only operator billing is permitted.",
      "recovery": "correctable",
      "details": {
        "rejected_billing": "agent",
        "suggested_billing": "operator"
      }
    }]
  }]
}
```

### Authorization (RBAC)

Returned when the caller is authenticated but lacks the specific scope for the request. Enforcement is seller-local; discoverability is via the `authorization` object on per-account entries in [`sync_accounts`](/docs/accounts/tasks/sync_accounts) and [`list_accounts`](/docs/accounts/tasks/list_accounts) responses. See [Caller authorization](/docs/accounts/overview#caller-authorization) for the full shape.

| Code                  | Recovery    | Description                                                                                                                                                                  | Resolution                                                                                                                                                                     |
| --------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `PERMISSION_DENIED`   | correctable | Generic authorization failure, or a required signed credential (e.g., `governance_context`) is missing, failed verification, or was issued for a different plan/seller/phase | Call `check_governance` to mint a valid token, or contact the seller to resolve the underlying permission                                                                      |
| `SCOPE_INSUFFICIENT`  | correctable | The invoked task is not in the caller's `allowed_tasks` for this account                                                                                                     | Re-read `authorization` on the account via `sync_accounts` or `list_accounts` to discover the caller's actual `allowed_tasks`; use a permitted task or request a broader scope |
| `READ_ONLY_SCOPE`     | correctable | Caller's scope is `read_only: true`; the invoked task would mutate state                                                                                                     | Use a non-mutating alternative, or request a scope that permits mutation                                                                                                       |
| `FIELD_NOT_PERMITTED` | correctable | A request field is not in the caller's `field_scopes` allowlist for this task                                                                                                | Drop the disallowed field, or request broader field scope                                                                                                                      |
| `AGENT_SUSPENDED`     | terminal    | Calling buyer agent's commercial relationship with this seller is temporarily paused                                                                                         | Surface to a human at the buyer; re-onboarding with the seller offline may resolve. The agent cannot unilaterally lift a suspension.                                           |
| `AGENT_BLOCKED`       | terminal    | Calling buyer agent's commercial relationship with this seller is permanently denied                                                                                         | Surface to a human at the buyer; relationships are reinstated only through offline operator action with the seller.                                                            |

Normative requirements:

* **`FIELD_NOT_PERMITTED` MUST populate `error.field`** with the exact offending field path (e.g., `packages[0].budget`, `end_time`). Without it, agents cannot reliably auto-recover by stripping the field and retrying. When multiple fields are disallowed, sellers SHOULD return one error per offending field so each `error.field` is unambiguous; if returning a single error, `error.details.fields` MAY contain the full list.
* **`SCOPE_INSUFFICIENT` SHOULD include `error.details.introspection_hint`** when the seller supports the `authorization` object on sync/list. Strawman shape: `{ "task": "sync_accounts", "account": { ... } }` pointing the caller at where to rediscover scope. This closes the "I hit an error; what do I do?" loop for coding agents.
* **All four codes MUST populate both the `adcp_error` envelope field and the payload `errors[]` array** per the two-layer model above — scope errors carry through to typed client libraries the same way schema errors do.

`SCOPE_INSUFFICIENT` vs. `PERMISSION_DENIED`: use `SCOPE_INSUFFICIENT` when the task itself is not granted to this caller on this account. Reserve `PERMISSION_DENIED` for credential-shaped failures (missing signed context, failed signature verification) and for generic seller policy rejections where scope is not the right abstraction.

`SCOPE_INSUFFICIENT` vs. `UNSUPPORTED_FEATURE`: when both apply — the seller doesn't implement the task AND the caller wouldn't be scoped for it anyway — sellers SHOULD return `SCOPE_INSUFFICIENT` because it is more actionable (the caller can request broader scope) than `UNSUPPORTED_FEATURE` (the caller must switch sellers). A seller that returns `UNSUPPORTED_FEATURE` for a task it *does* implement but is not exposed to this caller is leaking capability information the caller cannot act on.

`FIELD_NOT_PERMITTED` vs. `VALIDATION_ERROR`: the field is valid per the task schema and would be accepted from a differently-scoped caller; it is rejected specifically because this caller's `field_scopes` does not include it.

**About `recovery: correctable` on authorization errors.** All four authz codes classify as `correctable`, meaning *the request can be fixed and re-sent*. This does NOT mean the agent can fix the request autonomously — the "correction" for `SCOPE_INSUFFICIENT` and `READ_ONLY_SCOPE` is an out-of-band scope grant from the operator, which the agent cannot perform on its own. Agents SHOULD surface authz errors to the operator rather than auto-retrying against the same credential; a retry loop against a fixed scope is a bug, not defensive posture — with the narrow exception of the bounded disambiguation retries described below. Only `FIELD_NOT_PERMITTED` has an agent-autonomous recovery path (strip the disallowed field and resubmit).

**Retry disambiguation for `SCOPE_INSUFFICIENT` and `READ_ONLY_SCOPE`.** A single response with either code inside the seller's 300s authorization refresh window is observationally indistinguishable from cross-replica flicker — a transient infrastructure artifact that the spec forbids sellers from producing but buyers will still encounter in practice. Before classifying the error as a definitive `correctable` signal requiring operator intervention, buyers MAY exhaust a bounded retry budget (no more than 3 attempts, each separated by 1–5 seconds of jittered backoff) to establish whether the scope is genuinely insufficient. This is disambiguation logic — not recovery from a correctable error. After bounded retries exhaust without success, buyers MUST surface the error and MUST NOT autonomously continue retrying. This retry exception does NOT apply to `FIELD_NOT_PERMITTED` — the agent-autonomous strip-and-resubmit path supersedes it. See [Buyer response to SCOPE\_INSUFFICIENT within the refresh window](/docs/accounts/overview#buyer-response-to-scope_insufficient-within-the-refresh-window) for the full guidance including the `READ_ONLY_SCOPE` caveat on revocation scenarios.

**`FIELD_NOT_PERMITTED` — example envelope and payload populating both layers:**

```json theme={null}
{
  "adcp_error": {
    "code": "FIELD_NOT_PERMITTED",
    "message": "Caller's field_scopes for update_media_buy does not include this field",
    "recovery": "correctable",
    "field": "packages[0].budget"
  },
  "payload": {
    "errors": [
      {
        "code": "FIELD_NOT_PERMITTED",
        "message": "Caller's field_scopes for update_media_buy does not include this field",
        "recovery": "correctable",
        "field": "packages[0].budget",
        "details": {
          "task": "update_media_buy",
          "permitted_fields": ["reporting_webhook"]
        }
      }
    ]
  }
}
```

The `field` value identifies exactly what to strip; `details.permitted_fields` (optional, advisory) lists the allowlist for the offending task so an agent can verify before retrying. For `SCOPE_INSUFFICIENT`, populate `details.introspection_hint: { "task": "list_accounts", "account": { ... } }` to point the caller at where to rediscover scope.

#### Per-Agent Authorization Gate

The per-buyer-agent gate fires across three distinct rejection paths, each with its own discriminator so callers can dispatch without parsing prose:

| Code                | `details.scope`                                       | `details.reason` | Meaning                                                                                                                                                                                                                                                                  |
| ------------------- | ----------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `AGENT_SUSPENDED`   | — (no `details.scope`; the code is the discriminator) | —                | Agent's commercial relationship with the seller is temporarily paused. Re-onboarding may resolve; `recovery: "terminal"`.                                                                                                                                                |
| `AGENT_BLOCKED`     | — (no `details.scope`; the code is the discriminator) | —                | Agent's commercial relationship is permanently denied. No autonomous recovery; `recovery: "terminal"`.                                                                                                                                                                   |
| `PERMISSION_DENIED` | `"agent"`                                             | `"sandbox_only"` | Agent is provisioned for sandbox traffic only and the request was against a non-sandbox account. Per [`error-details/agent-permission-denied.json`](https://adcontextprotocol.org/schemas/v3/error-details/agent-permission-denied.json), `additionalProperties: false`. |

Per-agent commercial-status rejections (`AGENT_SUSPENDED`, `AGENT_BLOCKED`) follow the [`BILLING_NOT_PERMITTED_FOR_AGENT`](#billing-and-account-setup) precedent — the code itself is the discriminator and the response carries no explicit `error.details.scope` field. The `PERMISSION_DENIED + scope:"agent"` path is reserved for non-status provisioning gates whose `reason` is registered in the closed enum on `error-details/agent-permission-denied.json`. The `"agent"` value is a registered subset of the shared discriminator vocabulary in [`enums/error-scope.json`](https://adcontextprotocol.org/schemas/v3/enums/error-scope.json).

**When to mint a new code vs. extend the `reason` enum.** Lifecycle-terminal per-agent states (suspended, blocked, future deny-list-style states) get dedicated codes — they carry distinct `recovery` classifications and warrant first-class discriminators. Non-status provisioning gates and transient rejections extend the `error-details/agent-permission-denied.json` `reason` enum (e.g., `sandbox_only`) instead; throttling-shaped rejections reuse `RATE_LIMITED`, not a new per-agent code. The dedicated-code-per-state pattern composes only because the lifecycle vocabulary is finite — not as an open registry for every new gate.

**Migration note from the 3.0.5 placeholder.** 3.0.5 shipped `agent-permission-denied.json` with a `details.status: ["suspended", "blocked"]` axis as a placeholder for the per-agent lifecycle. 3.1 consolidates that placeholder into the dedicated `AGENT_SUSPENDED` / `AGENT_BLOCKED` codes — the `status` axis is removed from `agent-permission-denied.json` and the schema accepts only `scope:"agent" + reason:"sandbox_only"`. Sellers that integrated against the 3.0.5 placeholder MUST migrate to the dedicated codes; the placeholder shape is not preserved.

Normative requirements (apply to all three codes uniformly):

* **Uniform response without established agent identity.** Per-agent rejections are meaningful only when buyer-agent identity has been established via signed-request derivation per [Agent identity](/docs/building/by-layer/L1/security#agent-identity) or via a credential-to-agent mapping in the seller's onboarding record. Sellers MUST emit `AGENT_SUSPENDED` / `AGENT_BLOCKED` or `PERMISSION_DENIED` with `details.scope: "agent"` ONLY on that path; in all other cases — including bearer credentials not mapped to a specific agent record — sellers MUST return generic `PERMISSION_DENIED` and MUST omit `error.details.scope`. Returning the per-agent code (or per-agent scope) without established identity lets unauthenticated probes use the code-choice as a cross-tenant onboarding oracle, the same shape the [`*_NOT_FOUND` uniform-response rule](#standard-error-codes) and [`BILLING_NOT_PERMITTED_FOR_AGENT`](#billing-and-account-setup) close.
* **Channels covered by the omit-on-unestablished-identity rule.** The MUST applies across every observable channel — `error.code` / `error.message` / `error.field` / `error.details` (message MUST be generic on the unestablished-identity path); HTTP status, A2A `task.status.state`, and MCP `isError`; response headers (ETag, Cache-Control, per-agent rate-limit buckets, CDN tags); side effects (audit-log writes, webhook dispatch, background-job enqueues, per-agent quota counters, DB-shard routing, onboarding-record cache population); observability (logs, APM spans, Sentry/Datadog/Rollbar tags MUST NOT carry agent identity, agent-record references, or per-agent gate classification on the unestablished-identity path). Polymorphism is evaluated against the tool-schema's declared parameter shape *before* any onboarding-record lookup — a tool's declared shape MUST be identical across all callers regardless of whether identity has been established.
* **Latency parity.** Sellers MUST execute the same shape of onboarding-record lookup on both paths (e.g., resolve-then-authorize against an empty agent-record on the unmapped-credential path) so that lookup latency does not distinguish "credential is not mapped to an agent" from "credential maps to a suspended/blocked agent." Resolve-then-authorize, decision-of-equivalent-shape, the same posture the `*_NOT_FOUND` rule requires. Cache population MUST NOT be gated on identity establishment — the cache itself is otherwise an oracle through hit/miss timing.
* **Retry-counter side channel.** The "no autonomous retry" rule (below) MUST NOT itself become a side channel. Sellers MUST NOT increment a per-agent retry/backoff counter, mint a different `retry_after`, or surface different rate-limit headers on the unestablished-identity path than they would on a true-unauthenticated path. Buyer agents that comply with the no-retry rule MUST NOT have that compliance reflected in seller-side per-agent observability that another tenant can read.
* **No per-agent commercial state on any of the three paths.** None of `AGENT_SUSPENDED`, `AGENT_BLOCKED`, or `PERMISSION_DENIED + scope:"agent"` carry the agent's full permitted-billing subset, rate cards, payment terms, credit limit, billing entity, contact channels, custom reason strings, or any other per-agent commercial state — full-subset disclosure in a single probe is exactly the oracle the clamp prevents. `AGENT_SUSPENDED` / `AGENT_BLOCKED` carry no `error.details` payload; `PERMISSION_DENIED + scope:"agent"` MUST conform to `error-details/agent-permission-denied.json` (`scope` + registered `reason`, with `additionalProperties: false`). New `reason` values MUST be added to the schema's enum so cross-language SDKs can dispatch without parsing prose; future per-agent state surfaces (escalation channels, lift-policy URLs) belong on new dedicated codes with their own clamped details shapes, NOT by relaxing this shape. The schema's `additionalProperties: false` is a schema-validation guard; sellers MUST treat unrecognized or extension keys received from a seller-side composer as a defect and drop them before transmission.
* **No autonomous retry on the per-agent gate.** All three codes are terminal in practice: `AGENT_SUSPENDED` and `AGENT_BLOCKED` declare it directly via `recovery: "terminal"` in `enumMetadata`; the `PERMISSION_DENIED + scope:"agent"` path is `correctable` at the wire level (matching the registered `PERMISSION_DENIED` classification) but is terminal-pending-onboarding in practice — the agent cannot unilaterally lift a sandbox-only provisioning. Buyer agents MUST surface to a human at the buyer rather than auto-retrying on any of the three; re-attempts only reinforce the gate.

**Recovery dispatch — example.** Branch on `error.code` first; only fall through to `details.scope` for the `PERMISSION_DENIED` per-agent gate:

```javascript theme={null}
async function dispatchAuthzError(error) {
  // Per-agent commercial status — code is the discriminator, no details payload.
  if (error.code === 'AGENT_SUSPENDED' || error.code === 'AGENT_BLOCKED') {
    return surfaceToHuman({ code: error.code });
  }

  if (error.code !== 'PERMISSION_DENIED') throw error;

  // Generic credential-shaped failure — no scope on details.
  if (!error.details?.scope) {
    return refreshGovernanceContextAndRetry(error);
  }

  // Per-agent provisioning gate — terminal-pending-onboarding.
  if (error.details?.scope === 'agent') {
    const { reason } = error.details;
    // reason: 'sandbox_only'
    return surfaceToHuman({ code: error.code, reason });
  }

  throw error;  // Unknown scope — surface rather than guess.
}
```

**Example envelope** for `AGENT_SUSPENDED`:

```json theme={null}
{
  "adcp_error": {
    "code": "AGENT_SUSPENDED",
    "message": "Buyer agent's commercial relationship with this seller is suspended.",
    "recovery": "terminal"
  }
}
```

**Example envelope** for `PERMISSION_DENIED` with the sandbox-only provisioning gate:

```json theme={null}
{
  "adcp_error": {
    "code": "PERMISSION_DENIED",
    "message": "This buyer agent is provisioned for sandbox traffic only.",
    "recovery": "correctable",
    "details": {
      "scope": "agent",
      "reason": "sandbox_only"
    }
  }
}
```

The wire-level `recovery: "correctable"` on the sandbox-only path is the registered classification on `PERMISSION_DENIED` per [`error-code.json`](https://adcontextprotocol.org/schemas/v3/enums/error-code.json) `enumMetadata` — SDKs MUST NOT switch the registered value based on `details.scope`. Buyer agents MUST treat the rejection as terminal-pending-onboarding regardless of the wire-level `recovery` field, surface to a human, and not auto-retry. (For the suspended/blocked paths, the code itself carries `recovery: "terminal"` directly, so this caveat does not apply.)

### Request Validation

| Code                     | Recovery    | Description                                                  | Resolution                                                                                 |
| ------------------------ | ----------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------ |
| `INVALID_REQUEST`        | correctable | Request is malformed or violates schema constraints          | Check request parameters and fix                                                           |
| `UNSUPPORTED_FEATURE`    | correctable | Requested feature not supported by this seller               | Check `get_adcp_capabilities` and remove unsupported fields                                |
| `POLICY_VIOLATION`       | correctable | Request violates content or advertising policies             | Review policy requirements in the error details                                            |
| `COMPLIANCE_UNSATISFIED` | correctable | Required disclosure cannot be satisfied by the target format | Choose a format that supports the required disclosure capabilities                         |
| `GOVERNANCE_DENIED`      | correctable | A registered governance agent denied the transaction         | Restructure the buy, escalate to human spending authority, or contact the governance agent |

### Inventory and Products

| Code                         | Recovery    | Description                                                                                                                                           | Resolution                                                                                                                                                                                                  |
| ---------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PRODUCT_NOT_FOUND`          | correctable | Referenced product IDs are unknown or expired                                                                                                         | Remove invalid IDs, or re-discover with `get_products`                                                                                                                                                      |
| `PRODUCT_UNAVAILABLE`        | correctable | Product is sold out or no longer available                                                                                                            | Choose a different product                                                                                                                                                                                  |
| `PROPOSAL_EXPIRED`           | correctable | Referenced proposal has passed its `expires_at`                                                                                                       | Run `get_products` to get a fresh proposal                                                                                                                                                                  |
| `PROPOSAL_NOT_FOUND`         | correctable | `proposal_id` is unknown to the seller (never finalized, wrong tenant, or evicted from cache)                                                         | Re-issue `get_products` with `buying_mode: "refine"` + `action: "finalize"` to obtain a current proposal\_id                                                                                                |
| `MULTI_FINALIZE_UNSUPPORTED` | correctable | `refine[]` carried multiple `action: "finalize"` entries; seller cannot guarantee atomic multi-proposal commit                                        | Sequence single-proposal finalize calls (one finalize per `get_products` call)                                                                                                                              |
| `REQUOTE_REQUIRED`           | correctable | Requested update falls outside the envelope (budget, dates, volume, targeting) the original quote was priced against; `pricing_option` remains locked | Adjust the update to fit the current quote, rediscover products/terms, add packages when available, or create a separate media buy. 3.1 does not define an amendment-quote artifact for `update_media_buy`. |
| `SIGNAL_NOT_FOUND`           | correctable | Referenced signal does not exist in the catalog                                                                                                       | Verify `signal_id` via `get_signals`, or confirm availability from this agent                                                                                                                               |
| `AUDIENCE_TOO_SMALL`         | correctable | Audience segment below minimum size                                                                                                                   | Broaden targeting or upload more audience members                                                                                                                                                           |

### Budget and Creative

| Code                 | Recovery    | Description                                       | Resolution                                                                     |
| -------------------- | ----------- | ------------------------------------------------- | ------------------------------------------------------------------------------ |
| `BUDGET_TOO_LOW`     | correctable | Budget below seller's minimum                     | Increase budget or check `capabilities.media_buy.limits`                       |
| `BUDGET_EXHAUSTED`   | terminal    | Account or campaign budget fully spent            | Buyer must add funds or increase budget cap                                    |
| `CREATIVE_NOT_FOUND` | correctable | Referenced creative does not exist in the library | Verify `creative_id` via `list_creatives`, or register it via `sync_creatives` |
| `CREATIVE_REJECTED`  | correctable | Creative failed content policy review             | Revise per seller's `advertising_policies`                                     |

### System

| Code                  | Recovery             | Description                                                                                                                                                                                                                                                                                                                                                                                                                       | Resolution                                                                                                     |
| --------------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `RATE_LIMITED`        | transient            | Request rate exceeded                                                                                                                                                                                                                                                                                                                                                                                                             | Wait for `retry_after` seconds, then retry                                                                     |
| `SERVICE_UNAVAILABLE` | transient            | Seller service temporarily unavailable                                                                                                                                                                                                                                                                                                                                                                                            | Retry with exponential backoff                                                                                 |
| `STALE_RESPONSE`      | transient (advisory) | Non-fatal: seller served a populated payload from cache past its freshness target because an upstream/sub-agent was unreachable. Distinct from `SERVICE_UNAVAILABLE` (empty payload + fatal). `error.details` follows [`error-details/stale-response.json`](pathname:///schemas/v3/error-details/stale-response.json)                                                                                                             | Accept the cached payload, or retry later for fresh data — inspect `error.details.cache_age_seconds` to decide |
| `CONFIGURATION_ERROR` | terminal             | Seller-side deployment misconfiguration (e.g., missing `mock_upstream_url`, undeclared `upstream_url`, unset env var). Distinct from `SERVICE_UNAVAILABLE` (transient) and `INVALID_REQUEST` (buyer-fixable). Sellers MUST flip transport failure markers (HTTP 5xx, MCP `isError: true`, A2A `failed`); `error.message` carries operator-actionable detail and MUST NOT include credentials, connection strings, or stack traces | Surface to the seller's operator; do not auto-retry — retries will not resolve a misconfigured deployment      |
| `CONFLICT`            | transient            | Concurrent modification detected                                                                                                                                                                                                                                                                                                                                                                                                  | Re-read the resource and retry with current state                                                              |
| `REFERENCE_NOT_FOUND` | correctable          | Generic fallback for referenced resources without a dedicated not-found code. See [Not-found precedence](#standard-error-codes)                                                                                                                                                                                                                                                                                                   | Verify the identifier via the appropriate discovery task; prefer a resource-specific code when one exists      |

## Recovery Classification

Use the `recovery` field to determine how to handle errors:

| Recovery      | Meaning                                                                            | Action                                                |
| ------------- | ---------------------------------------------------------------------------------- | ----------------------------------------------------- |
| `transient`   | Temporary failure (rate limit, service unavailable, conflict)                      | Retry after `retry_after` or with exponential backoff |
| `correctable` | Request can be fixed and resent (invalid field, budget too low, creative rejected) | Modify the request and retry                          |
| `terminal`    | Requires human action (account suspended, payment required)                        | Escalate to a human operator                          |

For unknown `recovery` values (forward compatibility), treat as `terminal`.

```javascript theme={null}
function isRetryable(error) {
  // Use recovery field when available
  if (error.recovery) {
    return error.recovery === 'transient';
  }

  // Network errors are retryable
  if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT') {
    return true;
  }

  // Fall back to error code matching
  return ['RATE_LIMITED', 'SERVICE_UNAVAILABLE', 'CONFLICT'].includes(error.code);
}
```

## Retry Logic

The rules in this section bound every `transient`-classified error a caller may retry, including the `transient` default applied to unknown error codes under [§ Forward-compatible decoding](#forward-compatible-decoding-normative). A receiver that decodes an unknown code and falls back to `transient` MUST apply `maxRetries` and the jittered exponential-backoff schedule below; the open-enum decoding rule does not exempt receivers from the retry budget. A hostile or buggy sender emitting `code=GO_FOREVER, recovery=transient` cannot induce unbounded retries against a conformant client.

### Normative throttling behavior

These rules apply when a caller receives a throttling-category error (`RATE_LIMITED`, or any error whose `recovery` is `transient` and whose `details` conform to the [`rate-limited`](https://adcontextprotocol.org/schemas/v3/error-details/rate-limited.json) detail shape):

* Callers **MUST** honor `retry_after` when present and **MUST NOT** retry the same request sooner than the indicated number of seconds.
* Callers **SHOULD** use exponential backoff with jitter when `retry_after` is absent. A base of 2 seconds, a cap of 60 seconds, and ±25% jitter is a safe default.
* Callers **MUST NOT** treat non-throttling errors (e.g., `INVALID_REQUEST`, `CREATIVE_REJECTED`) as if they were throttled. Retrying a rejected-for-other-reasons response at backoff cadence is a bug, not a defensive posture.
* Callers **SHOULD** surface repeated throttling to their operator rather than retrying indefinitely; a persistent `RATE_LIMITED` response is a capacity or policy signal, not a transient blip.
* Sellers **SHOULD** return `RATE_LIMITED` with a populated `retry_after` rather than silently queuing or dropping requests, so well-behaved callers can back off intentionally.
* Sellers **MAY** populate the [`rate-limited`](https://adcontextprotocol.org/schemas/v3/error-details/rate-limited.json) detail shape (`limit`, `remaining`, `window_seconds`, `scope`) to let callers plan ahead rather than react per-429.

### Exponential Backoff

Implement exponential backoff for retryable errors:

```javascript theme={null}
async function retryWithBackoff(fn, options = {}) {
  const {
    maxRetries = 3,
    baseDelay = 1000,
    maxDelay = 60000
  } = options;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      if (!isRetryable(error) || attempt === maxRetries) {
        throw error;
      }

      // Use retry_after when available, otherwise exponential backoff
      const retryAfter = error.retry_after ||
        Math.min(baseDelay * Math.pow(2, attempt), maxDelay);

      // Add jitter to prevent thundering herd
      const jitter = retryAfter * (0.75 + Math.random() * 0.5);
      await sleep(jitter);
    }
  }
}
```

### Rate Limit Handling

```javascript theme={null}
async function handleRateLimit(error, retryFn) {
  if (error.recovery !== 'transient' &&
      error.code !== 'RATE_LIMITED') {
    throw error;
  }

  const retryAfter = error.retry_after || 60;
  console.log(`Rate limited. Waiting ${retryAfter} seconds...`);

  await sleep(retryAfter * 1000);
  return retryFn();
}
```

## Error Handling Patterns

### Basic Error Handler

```javascript theme={null}
async function handleAdcpError(error) {
  // Use recovery classification when available
  switch (error.recovery) {
    case 'transient':
      const delay = error.retry_after
        ? error.retry_after * 1000
        : 5000;
      await sleep(delay);
      return retry();

    case 'correctable':
      // Surface suggestion so the request can be fixed
      if (error.suggestion) {
        console.log('Suggestion:', error.suggestion);
      }
      if (error.field) {
        console.log('Problem field:', error.field);
      }
      throw error;

    case 'terminal':
      console.error('Terminal error:', error.message);
      throw error;
  }

  // Fall back to error code matching
  switch (error.code) {
    case 'AUTH_REQUIRED': {
      // Two sub-cases share this code; see the AUTH_REQUIRED warning above.
      const requestHadCredentials = Boolean(error.request_had_credentials);
      if (!requestHadCredentials) {
        await refreshCredentials();
        return retry();
      }
      // Credentials were presented and rejected — surface to operator.
      throw error;
    }

    case 'INVALID_REQUEST':
      console.error('Validation error:', error);
      throw error;

    default:
      console.error('AdCP error:', error);
      throw error;
  }
}
```

### User-Friendly Messages

Convert technical errors to user-friendly messages:

```javascript theme={null}
const USER_MESSAGES = {
  'RATE_LIMITED': 'Too many requests. Please wait a moment and try again.',
  'BUDGET_TOO_LOW': 'This is below the seller\'s minimum budget. Increase your budget.',
  'PRODUCT_NOT_FOUND': 'One or more products could not be found. Try searching again.',
  'ACCOUNT_SUSPENDED': 'Your account has been suspended. Contact the seller to resolve.',
  'SERVICE_UNAVAILABLE': 'The service is temporarily unavailable. Please try again in a few minutes.',
  'CREATIVE_REJECTED': 'Your creative did not pass policy review. Check the suggestion for details.',
  'AUDIENCE_TOO_SMALL': 'Your target audience is too small. Try broadening your targeting.'
};

function getUserMessage(code, fallbackMessage) {
  return USER_MESSAGES[code] || fallbackMessage || 'An unexpected error occurred. Please try again.';
}
```

### Structured Error Logging

Log errors with context for debugging:

```javascript theme={null}
function logError(error, context = {}) {
  console.error('AdCP Error:', {
    code: error.code,
    recovery: error.recovery,
    message: error.message,
    field: error.field,
    timestamp: new Date().toISOString(),
    ...context,
    // Don't log sensitive data
    // NO: credentials, briefs, PII
  });
}
```

## Webhook Error Handling

### Failed Webhook Delivery

When webhook delivery fails, fall back to polling:

```javascript theme={null}
class WebhookErrorHandler {
  async onDeliveryFailure(taskId, error) {
    console.warn(`Webhook delivery failed for ${taskId}:`, error);

    // Start polling as fallback
    this.startPolling(taskId);

    // Track failure for monitoring
    this.metrics.incrementCounter('webhook_failures');
  }

  async startPolling(taskId) {
    const response = await adcp.call('tasks/get', {
      task_id: taskId,
      include_result: true
    });

    if (['completed', 'failed', 'canceled'].includes(response.status)) {
      await this.processResult(taskId, response);
    } else {
      // Schedule next poll
      setTimeout(() => this.startPolling(taskId), 30000);
    }
  }
}
```

### Webhook Handler Errors

Handle errors in your webhook endpoint gracefully:

```javascript theme={null}
app.post('/webhooks/adcp', async (req, res) => {
  try {
    // Always respond quickly
    res.status(200).json({ status: 'received' });

    // Process asynchronously
    await processWebhookAsync(req.body);
  } catch (error) {
    // Log error but don't fail the response
    console.error('Webhook processing error:', error);

    // Move to dead letter queue for investigation
    await deadLetterQueue.add(req.body, error);
  }
});
```

## Recovery Strategies

### Context Recovery

If context expires, start a new conversation:

```javascript theme={null}
async function callWithContextRecovery(request) {
  try {
    return await adcp.call(request);
  } catch (error) {
    if (error.code === 'INVALID_REQUEST' &&
        error.message?.includes('context not found')) {
      // Clear stale context and retry
      delete request.context_id;
      return await adcp.call(request);
    }
    throw error;
  }
}
```

### Partial Success Handling

Some operations may partially succeed:

```json theme={null}
{
  "status": "completed",
  "message": "Created media buy with warnings",
  "media_buy_id": "mb_123",
  "errors": [
    {
      "code": "COMPLIANCE_UNSATISFIED",
      "message": "Required disclosure position not supported by one placement",
      "field": "packages[0].placements[2]",
      "suggestion": "Choose a format that supports the required disclosure positions"
    }
  ]
}
```

Handle partial success:

```javascript theme={null}
function handlePartialSuccess(response) {
  if (response.status === 'completed' && response.errors?.length) {
    // Show warnings to user
    for (const warning of response.errors) {
      showWarning(warning.message, warning.suggestion);
    }
  }

  // Continue with successful result
  return response;
}
```

## Governance Error Patterns

[`check_governance`](/docs/governance/campaign/tasks/check_governance) returns a `status` field rather than an error object. Governance results are not errors in the protocol sense — they are decisions. Handle them separately from AdCP task errors.

| Governance status | Meaning                   | Action                     |
| ----------------- | ------------------------- | -------------------------- |
| `approved`        | Plan passes governance    | Proceed                    |
| `conditions`      | Approved with constraints | Apply conditions, re-check |
| `denied`          | Plan violates governance  | Block the operation        |

If the governance agent needs human review internally (e.g., the action exceeds the agent's authority), `check_governance` behaves like any async task — it returns `submitted`/`working` status and eventually resolves to `approved` or `denied`. Handle this with the standard [async task lifecycle](/docs/building/by-layer/L3/task-lifecycle), not special-case logic.

Governance errors from the protocol layer (as opposed to governance decisions) use the standard error format. The most common:

| Code              | Recovery    | When it occurs                                        |
| ----------------- | ----------- | ----------------------------------------------------- |
| `PLAN_NOT_FOUND`  | correctable | `sync_plans` was not called before `check_governance` |
| `INVALID_REQUEST` | correctable | Missing required fields (e.g., `plan_id`, `caller`)   |
| `AUTH_REQUIRED`   | correctable | Governance agent requires authentication              |

## Configuration Error Patterns

`CONFIGURATION_ERROR` signals a seller-side deployment defect — an account declared with `mode: 'mock'` but no `mock_upstream_url`, a platform on `mode: 'live'` or `mode: 'sandbox'` with no `upstream_url`, a required environment variable unset on the seller process. The buyer cannot fix it; retries cannot resolve it; an operator at the seller has to. The catalog has no generic `INTERNAL_ERROR` code by design, and `CONFIGURATION_ERROR` is deliberately narrower — it covers the actionable slice where the remediation is "report this to the seller's operator." Opaque crashes that don't fit that profile remain catalog-uncoded; sellers MAY return platform-specific codes, and buyers fall back to the `recovery` classification per the [forward-compatibility rule](#recovery-classification).

### Aggregate signal: per-request terminal, per-seller outage

A single `CONFIGURATION_ERROR` is `terminal` for the request that received it — the buyer MUST surface to a human at the seller and MUST NOT auto-retry. Repeated `CONFIGURATION_ERROR` from the same seller in a short window is an operational signal of a different kind: a seller-side outage. Buyer-side dashboards and alerting SHOULD treat aggregate `CONFIGURATION_ERROR` rate per seller as an outage indicator (e.g., page on N occurrences in M minutes from a single seller), distinct from the per-request-terminal handling. This convergence matters because a buyer that buckets aggregate `CONFIGURATION_ERROR` with generic terminal errors loses the seller-isolated outage signal that motivated the code's existence.

### error.message: operator-actionable, not deployment-internal

The code itself is the discriminator — `CONFIGURATION_ERROR` carries no `error.details` shape (the [minimal-disclosure precedent](#per-agent-authorization-gate) of `AGENT_SUSPENDED` / `AGENT_BLOCKED` applies). `error.message` carries the diagnostic, and sellers SHOULD calibrate it to a level useful to a seller-side operator without leaking deployment internals to the buyer. The message is wire-visible — it MUST NOT include credentials, connection strings, full file paths, or stack traces.

Useful (operator can act, buyer learns nothing exploitable):

```json theme={null}
{
  "code": "CONFIGURATION_ERROR",
  "message": "account is mode='mock' but no mock_upstream_url declared in metadata; populate it in the AccountStore",
  "recovery": "terminal"
}
```

Not useful (the operator already knew there was a problem; the buyer learns where the seller's filesystem is):

```json theme={null}
{
  "code": "CONFIGURATION_ERROR",
  "message": "configuration error",
  "recovery": "terminal"
}
```

Leaks (don't):

```json theme={null}
{
  "code": "CONFIGURATION_ERROR",
  "message": "ECONNREFUSED postgres://admin:hunter2@10.0.1.42:5432/prod (at /opt/seller/src/db/pool.ts:127)",
  "recovery": "terminal"
}
```

## Best Practices

1. **Check `recovery` first** — it's the most reliable signal for how to handle an error
2. **Implement retries** — use exponential backoff for transient errors
3. **Respect rate limits** — honor `retry_after` values
4. **Handle unknown codes gracefully** — fall back to `error.recovery`; default to `transient` when absent (see [Forward-compatible decoding](#forward-compatible-decoding-normative))
5. **Log with context** — include `code`, `recovery`, and `field` for debugging
6. **Fallback strategies** — always have a backup (e.g., polling for webhooks)
7. **Don't retry terminal errors** — escalate to a human operator
8. **Handle partial success** — process warnings in successful responses

## Next Steps

* **Transport Bindings**: See [Transport Errors](/docs/building/operating/transport-errors) for how errors travel over MCP and A2A
* **Task Lifecycle**: See [Task Lifecycle](/docs/building/by-layer/L3/task-lifecycle) for status handling
* **Webhooks**: See [Webhooks](/docs/building/by-layer/L3/webhooks) for webhook error handling
* **Security**: See [Security](/docs/building/by-layer/L1/security) for authentication errors
