# Ad Context Protocol (AdCP)

> Generated at: 2026-05-28
> Library: @adcp/sdk v8.1.0-beta.15
> AdCP major version: 3
> Canonical URL: https://adcontextprotocol.github.io/adcp-client/llms.txt
> Note: the `Library` stamp reflects the package.json version at doc-generation time. The narrative below describes the surface that lands on the next-published minor — including any 6.7 helpers documented here ahead of the release tag.
> Note: generated error-code prose may include explicit SDK compatibility overlays applied by `scripts/lib/error-code-prose-overlays.ts` when bundled beta manifest wording lags SDK behavior.

## What is AdCP

AdCP is an open protocol for AI agents to buy, manage, and optimize advertising programmatically. It defines MCP tools that agents call on publisher ad servers — discover inventory, create media buys, sync creatives, manage brand safety, and track delivery. Every tool follows request/response JSON schemas; the TypeScript client wraps them with async task handling, conversation context, and governance middleware.

## Are you building a client or a server?

- **Client** (calling existing agents): Continue reading — the Quick Start below is for you.
- **Server** (implementing an agent that others call): Read `docs/guides/BUILD-AN-AGENT.md` and `docs/migration-5.x-to-6.x.md`. v6 recommended path:

```typescript
import { serve } from '@adcp/sdk';
import {
  createAdcpServerFromPlatform,
  definePlatform,
  defineSignalsPlatform,
} from '@adcp/sdk/server';

const platform = definePlatform({
  capabilities: {
    specialisms: ['signal-marketplace'] as const,
    pricingModels: ['cpm'] as const,
  },
  accounts: {
    resolve: async () => ({ id: 'acc_1', ctx_metadata: {} }),
  },
  signals: defineSignalsPlatform({
    getSignals: async (req, ctx) => ({ signals: [/* ... */], sandbox: true }),
    activateSignal: async (req, ctx) => ({ /* ... */ }),
  }),
});

serve(() => createAdcpServerFromPlatform(platform, {
  name: 'My Signals Agent',
  version: '1.0.0',
})); // http://localhost:3001/mcp
```

Compile-time enforcement: `RequiredPlatformsFor<S>` catches missing specialism methods. Capability projection auto-derives `get_adcp_capabilities` blocks (`audience_targeting`, `conversion_tracking`, `compliance_testing.scenarios`, etc.). Idempotency, RFC 9421 signing, async tasks, status normalization, and sync-completion webhook auto-emit are framework-owned.

Lower-level option: `createAdcpServer({ signals: { getSignals: ... } })` from `@adcp/sdk/server/legacy/v5` — handler-bag API. Still fully supported, the substrate the platform path calls into. Use when you need fine control over individual handlers, mid-migration from a v5 codebase, or custom-shaped tools the platform interface doesn't yet model. `wrapEnvelope(inner, { replayed, context, operationId })` from `@adcp/sdk/server` attaches protocol envelope fields with the per-error-code allowlist (IDEMPOTENCY_CONFLICT drops `replayed`).

**Identity helpers (drop `req: unknown` casts on inline platforms).** `definePlatform` / `defineSalesCorePlatform` / `defineSalesIngestionPlatform` / `defineSignalsPlatform` / `defineCreativeBuilderPlatform` / `defineCreativeAdServerPlatform` / `defineCampaignGovernancePlatform` / `defineContentStandardsPlatform` / `definePropertyListsPlatform` / `defineCollectionListsPlatform` / `defineBrandRightsPlatform` / `definePlatformWithCompliance` are pure identity helpers from `@adcp/sdk/server`. They force a concrete platform interface as the parameter type so TypeScript flows `req` / `ctx` typing into nested handler bodies. Class-pattern adopters with explicit property annotations (`sales: SalesCorePlatform<Meta> & SalesIngestionPlatform<Meta> = { ... }`) don't need them.

**Typed errors instead of `new AdcpError(code, ...)`.** `AuthMissingError`, `AuthInvalidError`, `PermissionDeniedError(action)`, `RateLimitedError(retryAfterSeconds)`, `ServiceUnavailableError`, `UnsupportedFeatureError(feature)`, `GovernanceDeniedError`, `PolicyViolationError`, `IdempotencyConflictError`, `InvalidRequestError`, `InvalidStateError`, plus the not-found family (`AccountNotFoundError`, `MediaBuyNotFoundError`, `PackageNotFoundError`, `ProductNotFoundError`, `CreativeNotFoundError`) and the budget / state family. `AuthRequiredError` remains as a deprecated `AUTH_REQUIRED` compatibility wrapper for older sellers; new seller code should use the split auth classes. Each maps to its wire error code with `recovery` baked in. Throw from platform methods. In `accounts.resolve`, use auth errors only for inbound authentication failures; missing sync linkage or unknown account references should stay `ACCOUNT_NOT_FOUND` / `null`.

**`composeMethod` cookbook.** To layer `before`/`after` hooks on a single platform method — short-circuit for caching, enrichment under `ext.*`, typed-error guards — use `composeMethod(inner, { before?, after? })` from `@adcp/sdk/server`. Stacking multiple guards: nest `composeMethod` calls (outer `before` runs first). Test patterns (mocking inner, asserting short-circuit, chained hooks, typed-error propagation): see [`docs/recipes/composeMethod-testing.md`](./recipes/composeMethod-testing.md). Pre-built `accounts.resolve` guards from the same package: `requireAccountMatch(predicate, opts)`, `requireAdvertiserMatch(getRoster, opts)`, `requireOrgScope(getAccountOrg, getCtxOrg, opts)`. Default deny returns `null` (indistinguishable from "not found"; guards against principal enumeration); opt in to `onDeny: 'throw'` for typed `PermissionDeniedError`.

**Four reference `AccountStore` shapes.** Pick the one whose onboarding model matches yours. **Shape A — `InMemoryImplicitAccountStore`**: `resolution: 'implicit'`, buyer-driven `sync_accounts` populates the auth-principal → accounts map. **Shape B — `createOAuthPassthroughResolver`**: `resolution: 'explicit'`, returns just the `resolve` function for adapters fronting an upstream OAuth listing endpoint (Snap, Meta, TikTok, LinkedIn — `extract bearer → GET /me/adaccounts → match by id`). **Shape C — `createRosterAccountStore`**: `resolution: 'explicit'`, returns a complete `AccountStore` for adopters who own the roster (storefront table, admin-UI-managed JSON). Supports `resolveWithoutRef` for tools that send no `account` field on the wire (`list_creative_formats`, `preview_creative`, `provide_performance_feedback`) — set it to return a synthetic publisher-wide entry instead of `null`. **Shape D — `createDerivedAccountStore`**: `resolution: 'derived'`, single-tenant agents where there is no `account_id` on the wire and the auth principal alone identifies the tenant (audiostack, flashtalking, single-namespace retail-media). Provide `toAccount(ctx)`; the factory still emits legacy-compatible `AUTH_REQUIRED` on missing-credential calls and ignores buyer-supplied `account_id` (single-tenant by definition). Buyer code must continue to handle `AUTH_REQUIRED` alongside `AUTH_MISSING` / `AUTH_INVALID`. All four live at `@adcp/sdk/server`.

**Multi-tenant.** Two helpers, pick by deployment shape. **Host-routed**: `createTenantRegistry({...})` — one server per tenant, tenant-id keyed lookup with `registry.get(tenantId)`. **Account-routed**: `createTenantStore({...})` — one server, per-entry tenant-isolation gate built in (cross-tenant entries on `upsert` / `syncGovernance` rejected with `PERMISSION_DENIED` BEFORE adopter callbacks run; fail-closed when the auth principal can't be resolved). `createTenantStore` mitigates the canonical multi-tenant write-across-tenants bug class at the SDK layer rather than relying on adopter discipline.

**`BuyerAgentRegistry`** — durable buyer-agent identity surface. `BuyerAgentRegistry.signingOnly({ resolveByAgentUrl })` (production target — only `http_sig` credentials route through), `bearerOnly({ resolveByCredential })` (pre-trust beta — bearer/api-key/oauth all route), `mixed(...)` (transition posture). Wrap with `BuyerAgentRegistry.cached(inner, { ttlSeconds })` for TTL + LRU + concurrent-resolve coalescing. The resolved `BuyerAgent` flows through `ctx.agent` to every `AccountStore` method (`resolve` / `upsert` / `list` / `syncGovernance` / `reportUsage` / `getAccountFinancials`) and to `tasks_get` polling. `BuyerAgent.status === 'suspended' | 'blocked'` triggers framework-level `PERMISSION_DENIED`. `BuyerAgent.sandbox_only: true` rejects requests against non-sandbox accounts. See [`docs/migration-buyer-agent-registry.md`](./migration-buyer-agent-registry.md) for the full surface.

**Lifecycle helpers.** `MEDIA_BUY_TRANSITIONS` and `CREATIVE_ASSET_TRANSITIONS` (the canonical state-graph maps the storyboard runner uses), plus `isLegalMediaBuyTransition(from, to)` / `assertMediaBuyTransition(from, to)` and the creative pair. `assertMediaBuyTransition` throws `AdcpError` with the spec-correct code (`NOT_CANCELLABLE` for the cancel-idempotency path, `INVALID_STATE` everywhere else). Production sellers that enforce transitions with these helpers cannot drift from conformance enforcement. `createMediaBuyStore({ store })` opt-in framework wiring handles the `packages[].targeting_overlay` echo contract on `get_media_buys` (sellers claiming `property-lists` / `collection-lists` MUST echo the persisted list reference).

**Breaking in 6.7 — audit before bumping.** (1) `accounts.resolution: 'implicit'` now actually refuses inline `{account_id}` references with `INVALID_REQUEST` (pre-6.7 the docstring claimed this but nothing checked it). Adopters whose callers passed inline `account_id` against an `'implicit'` platform must drop to `'explicit'` or fix callers to use `sync_accounts` first. (2) `SalesPlatform` is now structurally `SalesCorePlatform & SalesIngestionPlatform` with all methods individually optional. Adopters with `: SalesPlatform<Meta>` field annotations claiming `sales-non-guaranteed` / `-guaranteed` / `-broadcast-tv` / `-catalog-driven` need to switch the annotation to `: SalesCorePlatform<Meta> & SalesIngestionPlatform<Meta>` (or use `defineSalesCorePlatform` + `defineSalesIngestionPlatform` spread). Self-announcing under `tsc --noEmit`. Walled-garden CAPI specialisms (`sales-social`) drop ~40 LOC of stub-throw boilerplate. Full migration recipe at [`docs/migration-6.6-to-6.7.md`](./migration-6.6-to-6.7.md).

**`Account<TCtxMeta>` v3 wire fields.** `Account` gained `billing_entity`, `rate_card`, `payment_terms`, `credit_limit`, `setup` (drives `pending_approval` → `active` lifecycle), `account_scope`, `governance_agents`, and `reporting_bucket` — all optional. `billing_entity.bank` and `governance_agents[i].authentication.credentials` are stripped on emit per spec; `Account.authInfo` is now optional. `AccountStore.upsert` / `list` / `syncGovernance` accept an optional `ResolveContext` second argument carrying `authInfo` / `toolName` / `agent` for principal-keyed gating.

**`refAccountId(ref)`** narrows `AccountReference` to its `account_id` arm without casting (returns `undefined` for missing refs, `{brand, operator}` arms, sandbox arms). `narrowAccountRef(ref)` returns the typed arm or `null` for full discriminated-union narrowing. **`NoAccountCtx<TCtxMeta>`** is the request-context type for tools whose wire request doesn't carry an `account` field (`previewCreative`, `listCreativeFormats`, `providePerformanceFeedback`); `ctx.account` is `Account<TCtxMeta> | undefined` and adopters either return a singleton from `accounts.resolve(undefined)` or guard with `if (ctx.account == null) ...`.

**Validation hints on every `VALIDATION_ERROR` envelope.** `ValidationIssue` carries `hint` (one-sentence curated recipe for known shape gotchas — `activation_key` discriminator nesting, `account` discriminator merging, `budget` shape, `format_id` object, VAST/DAAST `delivery_type`, missing `idempotency_key`, log_event/CAPI projection), `discriminator` (which `oneOf` branch the validator inferred), and `schemaId` (the `$id` of the rejecting schema). Buyer-side recovery order: `hint` first, then `discriminator`, then `variants`, then `pointer` + `keyword`. `oneOf` near-miss diagnostics now point at the Success-arm residuals when a Success-vs-Error envelope payload populates Success-only fields.

**Other adopter-facing surfaces.** `DecisioningPlatform.instructions` accepts a function form (`(ctx: SessionContext) => string | undefined`) for per-session prose under `serve({ reuseAgent: false })`. `listCreativeFormats?` is now typed on `CreativeBuilderPlatform` and `CreativeAdServerPlatform` (drops the v5 `opts.creative.listCreativeFormats` escape hatch). `update_rights` is a first-class brand-rights tool with `creative_approval` webhook builders. `@adcp/sdk/upstream-recorder` is a sandbox-only producer-side middleware for the `query_upstream_traffic` storyboard check; `@adcp/sdk/mock-server` is a public sub-export for in-process integration tests. `runStoryboard({ agents })` routes per-specialism storyboard steps to multiple agents (matching `/sales`, `/signals`, `/governance`, `/creative`, `/brand` topology). `media_buy_ids[]` fan-out on `getMediaBuyDelivery` / `getCreativeDelivery` is platform-side pass-through (framework hands the array as-is); a dev-mode warning fires when handlers return fewer rows than requested.

**Don't put credentials in `ctx_metadata`.** Wire-strip protects buyer responses but not server-side log lines, error envelopes, heap dumps, or adopter-generated strings. Re-derive bearers per request from `ctx.authInfo` + your token cache; embed only non-secret upstream IDs in `ctx_metadata`. See [`docs/guides/CTX-METADATA-SAFETY.md`](./guides/CTX-METADATA-SAFETY.md).

## Quick Start (Client)

```typescript
import { ADCPMultiAgentClient } from '@adcp/sdk';

const client = ADCPMultiAgentClient.simple('https://agent.example.com/mcp/', {
  authToken: process.env.ADCP_TOKEN,
});
const agent = client.agent('default-agent');

// Discover products
const products = await agent.getProducts({ buying_mode: 'brief', brief: 'coffee brands' });
if (products.status === 'completed') console.log(products.data.products);

// Create a media buy
const buy = await agent.createMediaBuy({
  account: { account_id: 'acct_1' },
  brand: { domain: 'coffee.example.com' },
  start_time: 'asap',
  end_time: '2026-06-01T00:00:00Z',
  packages: [{ buyer_ref: 'pkg-1', product_id: 'prod_1', pricing_option_id: 'cpm_1', budget: 5000 }],
});
```

## Canonical Reference Resolver

`format_schema` and `platform_extensions` references use immutable `{ uri, digest }` pointers. Use `createCanonicalReferenceResolver` from `@adcp/sdk/canonical-references` instead of raw fetches; it applies SSRF-safe DNS-pinned fetches, redirect blocking, timeout/body caps, SHA-256 verification, structured non-throwing statuses, and caller-owned policy-scoped caching.

```typescript
import { createCanonicalReferenceResolver } from '@adcp/sdk/canonical-references';

const resolver = createCanonicalReferenceResolver();
const formatSchemaRef = {
  uri: 'https://publisher.example-ad.com/schemas/slot.json',
  digest: 'sha256:<64 lowercase hex chars>',
};
const result = await resolver.resolveFormatSchema(formatSchemaRef, {
  externalRefDigests: {
    'https://publisher.example-ad.com/shared-slot.json': 'sha256:<64 lowercase hex chars>',
  },
});

if (!result.ok) {
  if (result.error.code === 'digest_mismatch') throw new Error('Reference substitution detected');
  if (result.error.retryable) /* retry later */;
}
```

For `format_schema`, the resolver requires an explicit `$schema`, validates Draft-07 / Draft 2020-12 JSON Schema, inlines only pinned safe `$ref` targets, rejects known catastrophic regex patterns with `error.code: 'budget_exceeded'`, and returns `schemaMeta` on success. Failure statuses are coarse (`unresolvable`, `invalid_document`, `invalid_schema`, `digest_mismatch`, `blocked_unsafe_url`, `invalid_ref`); branch on `error.code` for precise handling. See `docs/guides/CANONICAL-REFERENCE-RESOLVER.md`.

## Transport auth

AdCP is auth-scheme-agnostic at the transport layer. The protocol carries JSON-RPC over HTTP; how the outer envelope is gated is an operator-private deployment choice — bearer tokens, OAuth, mTLS, AWS SigV4 at the edge, an IP allow-list, or RFC 7617 HTTP Basic when the agent sits behind an API gateway with a BasicAuthentication policy (Apigee, Kong, AWS API Gateway, nginx `auth_basic`) are all valid. `get_adcp_capabilities` does NOT advertise the accepted auth schemes; encoding every gateway permutation in the capability payload would couple the protocol to infrastructure choices that change between deployments.

Auth-scheme discovery, when needed, flows through `WWW-Authenticate` (RFC 9110 §11.6.1) and Protected Resource Metadata (RFC 9728) — both consumed by the SDK's auth-diagnostics path. Basic-fronted agents emit `WWW-Authenticate: Basic realm="…"` on a 401; consumers (SDK callers, the CLI's 401-bounce path, LLM agents) should branch on the challenge scheme rather than retrying Bearer indefinitely.

The TypeScript SDK speaks both schemes today. Programmatically: `createTestClient({ auth: { type: 'basic', username, password } })` (RFC 7617) and `createTestClient({ auth: { type: 'bearer', token } })`. From the CLI: `--auth-scheme basic` opts into Basic and `--auth <user:pass>` carries the credential; the default `bearer` remains unchanged.

## Error Handling

When `result.success` is `false`, use `result.adcpError` for programmatic handling:

- `result.error` — Human-readable string (e.g., `"RATE_LIMITED: Too many requests"`)
- `result.adcpError.code` — Error code (e.g., `RATE_LIMITED`, `INVALID_REQUEST`)
- `result.adcpError.recovery` — `'transient'` (retry), `'correctable'` (fix request), or `'terminal'` (give up)
- `result.adcpError.retryAfterMs` — Milliseconds to wait before retrying
- `result.adcpError.field` / `result.adcpError.suggestion` — Hints for correctable errors
- `result.adcpError.synthetic` — `true` when inferred from unstructured text
- `result.correlationId` — Correlation ID for tracing across agents

Use `isRetryable(result)` and `getRetryDelay(result)` for retry logic. `TaskResult` is a discriminated union — `if (result.success)` narrows `data` to `T`; `if (!result.success)` guarantees `error: string` and `status: 'failed'`.

```typescript
if (!result.success) {
  if (isRetryable(result)) {
    await sleep(getRetryDelay(result)); // ms, defaults to 5000
  } else if (result.adcpError?.recovery === 'correctable') {
    console.log('Fix:', result.adcpError.suggestion, 'Field:', result.adcpError.field);
  } else {
    console.error(result.error, 'Correlation:', result.correlationId);
  }
}
```

For exhaustive handling across all seven statuses, prefer the `match()` dispatcher (fluent method on every result returned from the SDK, or free function import):

```typescript
const label = result.match!({
  completed: r => `OK: ${JSON.stringify(r.data)}`,
  failed: r => `Error: ${r.adcpError?.code ?? r.error}`,
  submitted: r => `Pending: poll ${r.metadata.taskId}`,
  'governance-denied': r => `Denied: ${r.adcpError?.code ?? r.error}`,
  working: r => `Running: ${r.metadata.taskId}`,
  'input-required': r => `Needs input: ${r.metadata.inputRequest?.question}`,
  deferred: r => `Deferred: ${r.deferred?.token}`,
});
// Optional `_` catchall makes every arm optional:
// const label = result.match!({ completed: r => JSON.stringify(r.data), _: r => r.status });
```

TypeScript enforces exhaustiveness at compile time when the `_` catchall is omitted — missing an arm is a type error, not a runtime surprise. The `!` is because `TaskResultBase.match` is declared optional so hand-constructed result literals (tests, middleware) stay valid; every result returned from the SDK has `.match` attached. For hand-constructed literals, use the free function `match(result, handlers)` or call `attachMatch(result)` first.

## Idempotency (mutating requests)

AdCP v3 requires `idempotency_key` on every mutating request (`create_media_buy`, `update_media_buy`, `activate_signal`, all `sync_*`, `si_send_message`, etc.). The SDK auto-generates a UUID v4 when callers don't supply one, reuses it across internal retries, and surfaces it on the result:

```typescript
const result = await client.createMediaBuy({ account, brand, start_time, end_time, packages });
result.metadata.idempotency_key  // key that was sent (auto-generated or caller-supplied)
result.metadata.replayed         // true if this was a cached replay from a prior retry
```

**Two things agents with side effects MUST handle:**

1. **Side-effect suppression on `replayed: true`.** If your agent emits notifications, writes LLM memory, or fires downstream tool calls on the response, check `result.metadata.replayed` before acting. A cached replay means the side effects already fired on the original call.

```typescript
if (result.success && !result.metadata.replayed) {
  await notify(`Campaign ${result.data.media_buy_id} created`);
  await memory.write({ campaign_id: result.data.media_buy_id });
}
```

2. **Agent re-plan vs. network retry.** A network retry (same bytes, socket timeout) reuses the same key — the SDK handles this. An agent re-plan (LLM re-ran its planner and produced a different payload) means a NEW intent — mint a fresh key by calling the method again without passing one. Reusing the prior key with a different payload returns `IdempotencyConflictError`.

**Typed errors:** on failure, `result.errorInstance` carries a typed `ADCPError` subclass for codes with dedicated classes — currently `IdempotencyConflictError` and `IdempotencyExpiredError`. Prefer `instanceof` checks over switching on `adcpError.code` strings.

```typescript
import { IdempotencyConflictError, IdempotencyExpiredError } from '@adcp/sdk';

if (result.errorInstance instanceof IdempotencyConflictError) {
  // Agent re-planned with different payload. Retry with a fresh key.
  // result.errorInstance.idempotencyKey carries the key the server omitted.
}
if (result.errorInstance instanceof IdempotencyExpiredError) {
  // Key past replay window. If you know the prior call succeeded, look up
  // by natural key (e.g., get_media_buys by context.internal_campaign_id).
  // Otherwise mint a fresh key.
}
```

**BYOK** (persist keys in your DB across process restarts): you own the replay-window boundary. Ask the client for the seller's declared TTL:

```typescript
const ttl = await client.getIdempotencyReplayTtlSeconds();
// Returns the declared number. Throws ConfigurationError if the seller is v3
// but omits adcp.idempotency.replay_ttl_seconds — the SDK does NOT default to
// 24h, because a silent default misleads retry-sensitive flows. Returns
// undefined on v2 sellers (pre-idempotency-envelope).
```

Pass your persisted key with `useIdempotencyKey(key)` — it validates against the spec pattern (`^[A-Za-z0-9_.:-]{16,255}$`) before the network round-trip:

```typescript
import { useIdempotencyKey } from '@adcp/sdk';
const key = await db.getOrCreateIdempotencyKey(campaign.id);
await client.createMediaBuy({ ...params, ...useIdempotencyKey(key) });
```

**Crash-recovery cookbook.** For an end-to-end recipe (natural-key lookup after restart, `IdempotencyConflictError` / `IdempotencyExpiredError` handling, `metadata.replayed` as side-effect gate, Postgres schema), see [`docs/guides/idempotency-crash-recovery.md`](./guides/idempotency-crash-recovery.md).

## ext.adcp Extension Namespace

**`ext.adcp.*` namespace.** The SDK reserves keys under `ext.adcp.*` for read-by-agent extensions that don't yet warrant their own AdCP spec field. Agents that recognize a key act on it; agents that don't recognize it ignore it silently (per AdCP `ext` semantics: accepted-without-error). The namespace is transport-neutral — it travels in the `ext` envelope field on both MCP and A2A transports. Keys in this namespace are hints **inbound to seller/responder agents** from the SDK or test tooling; **buyer agents building production flows MUST NOT emit `ext.adcp.*` keys**.

| Key | Stamped by | Purpose |
|-----|-----------|---------|
| `ext.adcp.disable_sandbox` | `adcp storyboard run --no-sandbox` | Hint (value: `true`) to bypass internal sandbox routing and exercise real adapter paths. Seller agents that honor this key serve production-shaped responses regardless of internal sandbox heuristics (env-var fallbacks, brand-domain detection, fixture substitutes). |

Third-party extensions MUST use a distinct namespace (e.g. `ext.com.example.*`) to avoid collisions with future `ext.adcp.*` keys.

## Tools

Every tool is an MCP tool called via `agent.<methodName>(params)`. Returns `TaskResult<T>` with `status`, `data`, `error`, `adcpError`, `correlationId`, `deferred`, or `submitted`.

### Protocol

#### `get_adcp_capabilities`

Request parameters for cross-protocol capability discovery.

**Request:**
- Optional: `protocols: string[]`, `context: Context`

**Response (success branch):**
- Required: `adcp: object`, `supported_protocols: string[]`
- Optional: `account: object`, `media_buy: object`, `signals: object`, `governance: object`, `sponsored_intelligence: object`, `brand: object`, `creative: object`, `request_signing: object`, +12 more

### Account Management

#### `list_accounts`

Request parameters for listing accounts accessible to the authenticated agent.

**Request:**
- Optional: `account: Account Ref`, `status: 'active' | 'pending_approval' | 'rejected' | 'payment_required' | 'suspended' | 'closed'`, `pagination: Pagination Request`, `sandbox: boolean`, `context: Context`

**Response (success branch):**
- Required: `accounts: object[]`
- Optional: `errors: object[]`, `pagination: Pagination Response`, `context: Context`

#### `sync_accounts`

Request parameters for syncing advertiser accounts with a seller.

**Request:**
- Required: `idempotency_key: string`, `accounts: object[]`
- Optional: `delete_missing: boolean`, `dry_run: boolean`, `push_notification_config: Push Notification Config`, `context: Context`

**Response (success branch):**
- Required: `accounts: object[]`
- Optional: `dry_run: boolean`, `context: Context`

#### `sync_governance`

Request parameters for registering governance agent endpoints on accounts.

**Request:**
- Required: `idempotency_key: string`, `accounts: object[]`
- Optional: `context: Context`

**Response (success branch):**
- Required: `accounts: object[]`
- Optional: `context: Context`

#### `report_usage`

Request parameters for reporting vendor service consumption after delivery.

**Request:**
- Required: `idempotency_key: string`, `reporting_period: Datetime Range`, `usage: object[]`
- Optional: `context: Context`

**Response (success branch):**
- Required: `accepted: integer`
- Optional: `errors: object[]`, `sandbox: boolean`, `context: Context`

#### `get_account_financials`

Request parameters for querying financial status of an operator-billed account.

**Request:**
- Required: `account: Account Ref`
- Optional: `period: Date Range`, `context: Context`

**Response (success branch):**
- Required: `account: Account Ref`, `currency: string`, `period: Date Range`, `timezone: string`
- Optional: `spend: object`, `credit: object`, `balance: object`, `payment_status: 'current' | 'past_due' | 'suspended'`, `payment_terms: Payment Terms`, `invoices: object[]`, `context: Context`

**Deep dive:**
- docs/getting-started.md — authentication and account setup

### Media Buying

#### `get_products`

Request parameters for discovering available advertising products.

**Request:**
- Required: `buying_mode: 'brief' | 'wholesale' | 'refine'`
- Optional: `brief: string`, `refine: object[]`, `brand: Brand Ref`, `catalog: Catalog`, `account: Account Ref`, `preferred_delivery_types: object[]`, `filters: Product Filters`, `property_list: Property List Ref`, +7 more

**Response (success branch):**
- Optional: `products: object[]`, `extensions: object`, `proposals: object[]`, `errors: object[]`, `property_list_applied: boolean`, `catalog_applied: boolean`, `refinement_applied: object[]`, `incomplete: object[]`, +8 more

**Watch out:**
- `cache_scope` is required whenever the response includes `products` or `unchanged: true`. Use `public` for the universal rate card and `account` for account-specific rate cards or pricing overlays.
- SDK server handlers may omit `cache_scope` only for no-account product feeds; the framework can safely infer `public` only when there is no inline account and no auth-derived/resolved account.

#### `list_creative_formats`

Request parameters for discovering format IDs and creative agents supported by this sales agent.

**Request:**
- Optional: `format_ids: object[]`, `asset_types: object[]`, `max_width: integer`, `max_height: integer`, `min_width: integer`, `min_height: integer`, `is_responsive: boolean`, `name_search: string`, +9 more

**Response (success branch):**
- Required: `formats: object[]`
- Optional: `source: 'publisher' | 'aao_mirror' | 'agent_derived'`, `creative_agents: object[]`, `errors: object[]`, `pagination: Pagination Response`, `sandbox: boolean`, `context: Context`

**Watch out:**
- Each `renders[]` entry satisfies a `oneOf` — exactly one of `dimensions` (object) OR `parameters_from_format_id: true`. A render with only `{ role }` (or `{ role, duration_seconds }`) fails validation.
- Use the typed factories from `@adcp/sdk`: `displayRender({ role, dimensions })` for display/video; `parameterizedRender({ role })` for audio and template formats (auto-injects `parameters_from_format_id: true`).
- Audio formats (`type: "audio"`) have no width/height — declare `renders: [parameterizedRender({ role: "primary" })]` and encode duration/codec in `format_id.parameters` (declared via `accepts_parameters`).

#### `create_media_buy`

Request parameters for creating a media buy.

**Request:**
- Required: `idempotency_key: string`, `account: Account Ref`, `brand: Brand Ref`, `start_time: Start Timing`, `end_time: string`
- Optional: `plan_id: string`, `proposal_id: string`, `total_budget: object`, `packages: object[]`, `advertiser_industry: Advertiser Industry`, `invoice_recipient: Business Entity`, `io_acceptance: object`, `po_number: string`, +5 more

**Response (success branch):**
- Required: `media_buy_id: string`, `confirmed_at: string,null`, `revision: integer`, `packages: object[]`
- Optional: `account: Account`, `invoice_recipient: Business Entity`, `media_buy_status: Media Buy Status`, `status: Media Buy Status`, `creative_deadline: string`, `currency: string`, `total_budget: number`, `valid_actions: object[]`, +4 more

**Watch out:**
- Server handlers should return business lifecycle state as `media_buy_status`. The framework owns the task envelope `status`; do not return top-level `status` as the media-buy state.

#### `update_media_buy`

Request parameters for updating campaign and package settings.

**Request:**
- Required: `account: Account Ref`, `media_buy_id: string`, `idempotency_key: string`
- Optional: `revision: integer`, `paused: boolean`, `canceled: 'true'`, `cancellation_reason: string`, `start_time: Start Timing`, `end_time: string`, `packages: object[]`, `invoice_recipient: Business Entity`, +4 more

**Response (success branch):**
- Required: `media_buy_id: string`, `revision: integer`
- Optional: `media_buy_status: Media Buy Status`, `status: Media Buy Status`, `currency: string`, `total_budget: number`, `implementation_date: string,null`, `invoice_recipient: Business Entity`, `affected_packages: object[]`, `valid_actions: object[]`, +3 more

**Watch out:**
- Server handlers should return business lifecycle state as `media_buy_status`. The framework owns the task envelope `status`; do not return top-level `status` as the media-buy state.

#### `get_media_buys`

Request parameters for retrieving media buy status, creative approvals, and delivery snapshots.

**Request:**
- Optional: `account: Account Ref`, `media_buy_ids: string[]`, `status_filter: Media Buy Status | object[]`, `include_snapshot: boolean`, `include_history: integer`, `include_webhook_activity: boolean`, `webhook_activity_limit: integer`, `pagination: Pagination Request`, +1 more

**Response (success branch):**
- Required: `media_buys: object[]`
- Optional: `errors: object[]`, `pagination: Pagination Response`, `sandbox: boolean`, `context: Context`

#### `get_media_buy_delivery`

Request parameters for retrieving comprehensive delivery metrics.

**Request:**
- Optional: `account: Account Ref`, `media_buy_ids: string[]`, `status_filter: Media Buy Status | object[]`, `start_date: string`, `end_date: string`, `include_package_daily_breakdown: boolean`, `time_granularity: Reporting Frequency`, `include_window_breakdown: boolean`, +3 more

**Response (success branch):**
- Required: `reporting_period: object`, `currency: string`, `media_buy_deliveries: object[]`
- Optional: `notification_type: 'scheduled' | 'final' | 'delayed' | 'adjusted' | 'window_update'`, `partial_data: boolean`, `unavailable_count: integer`, `sequence_number: integer`, `next_expected_at: string`, `attribution_window: Attribution Window`, `aggregated_totals: object`, `errors: object[]`, +2 more

#### `provide_performance_feedback`

Request parameters for sharing performance outcomes with publishers.

**Request:**
- Required: `media_buy_id: string`, `idempotency_key: string`, `measurement_period: Datetime Range`, `performance_index: number`
- Optional: `package_id: string`, `creative_id: string`, `metric_type: Metric Type`, `feedback_source: Feedback Source`, `context: Context`

**Response (success branch):**
- Required: `success: 'true'`
- Optional: `sandbox: boolean`, `context: Context`

#### `sync_event_sources`

Request parameters for configuring event sources on an account.

**Request:**
- Required: `idempotency_key: string`, `account: Account Ref`
- Optional: `event_sources: object[]`, `delete_missing: boolean`, `context: Context`

**Response (success branch):**
- Required: `event_sources: object[]`
- Optional: `sandbox: boolean`, `context: Context`

#### `log_event`

Request parameters for logging conversion or marketing events.

**Request:**
- Required: `event_source_id: string`, `events: object[]`, `idempotency_key: string`
- Optional: `test_event_code: string`, `context: Context`

**Response (success branch):**
- Required: `events_received: integer`, `events_processed: integer`
- Optional: `partial_failures: object[]`, `warnings: string[]`, `match_quality: number`, `sandbox: boolean`, `context: Context`

#### `sync_audiences`

Request parameters for managing CRM-based audiences on an account.

**Request:**
- Required: `idempotency_key: string`, `account: Account Ref`
- Optional: `audiences: object[]`, `delete_missing: boolean`, `context: Context`

**Response (success branch):**
- Required: `audiences: object[]`
- Optional: `sandbox: boolean`, `context: Context`

#### `sync_catalogs`

Request parameters for syncing catalog feeds (products, inventory, stores, promotions, offerings) with approval workflow.

**Request:**
- Required: `idempotency_key: string`, `account: Account Ref`
- Optional: `catalogs: object[]`, `catalog_ids: string[]`, `delete_missing: boolean`, `dry_run: boolean`, `validation_mode: Validation Mode`, `push_notification_config: Push Notification Config`, `context: Context`

**Response (success branch):**
- Required: `catalogs: object[]`
- Optional: `dry_run: boolean`, `sandbox: boolean`, `context: Context`

**Deep dive:**
- docs/getting-started.md — installation, auth, basic usage
- docs/guides/ASYNC-DEVELOPER-GUIDE.md — async task patterns (submitted, deferred, input-required)
- docs/guides/PUSH-NOTIFICATION-CONFIG.md — webhook setup for delivery reports
- docs/guides/REAL-WORLD-EXAMPLES.md — end-to-end buying flows

### Creative

#### `build_creative`

Request parameters for AI-powered creative generation.

**Request:**
- Required: `idempotency_key: string`
- Optional: `message: string`, `creative_manifest: Creative Manifest`, `creative_id: string`, `concept_id: string`, `media_buy_id: string`, `package_id: string`, `target_format_id: Format Id`, `target_format_ids: object[]`, +10 more

**Response (success branch):**
- Required: `creative_manifest: Creative Manifest`
- Optional: `sandbox: boolean`, `expires_at: string`, `preview: object`, `preview_error: Error`, `pricing_option_id: string`, `vendor_cost: number`, `currency: string`, `consumption: Creative Consumption`, +1 more

**Watch out:**
- Response is ALWAYS `{ creative_manifest }` (single) or `{ creative_manifests }` (multi). Platform-native fields at the top level (`tag_url`, `creative_id`, `media_type`) are invalid.
- Use `buildCreativeResponse({ creative_manifest })` / `buildCreativeMultiResponse({ creative_manifests })` from `@adcp/sdk/server` to enforce the shape at compile time.
- Each asset under `creative_manifest.assets` needs an `asset_type` discriminator — use the factories: `imageAsset`, `videoAsset`, `audioAsset`, `htmlAsset`, `urlAsset`, `textAsset` (or `Asset.image(...)`).

#### `preview_creative`

Request parameters for generating creative previews.

**Request:**
- Required: `request_type: 'single' | 'batch' | 'variant'`
- Optional: `creative_manifest: Creative Manifest`, `format_id: Format Id`, `inputs: object[]`, `template_id: string`, `quality: Creative Quality`, `output_format: Preview Output Format`, `item_limit: integer`, `requests: object[]`, +3 more

**Response (success branch):**
- Required: `response_type: 'single'`, `previews: object[]`
- Optional: `interactive_url: string`, `expires_at: string`, `context: Context`

**Watch out:**
- Each `renders[]` entry is a oneOf on `output_format` — use `urlRender({...})`, `htmlRender({...})`, or `bothRender({...})` to inject the discriminator and require the matching `preview_url`/`preview_html` field.

#### `list_creative_formats`

Request parameters for discovering creative formats from this creative agent.

**Request:**
- Optional: `format_ids: object[]`, `type: 'audio' | 'video' | 'display' | 'dooh'`, `asset_types: string[]`, `max_width: integer`, `max_height: integer`, `min_width: integer`, `min_height: integer`, `is_responsive: boolean`, +10 more

**Response (success branch):**
- Required: `formats: object[]`
- Optional: `creative_agents: object[]`, `errors: object[]`, `pagination: Pagination Response`, `context: Context`

**Watch out:**
- Each `renders[]` entry satisfies a `oneOf` — exactly one of `dimensions` (object) OR `parameters_from_format_id: true`. A render with only `{ role }` (or `{ role, duration_seconds }`) fails validation.
- Use the typed factories from `@adcp/sdk`: `displayRender({ role, dimensions })` for display/video; `parameterizedRender({ role })` for audio and template formats (auto-injects `parameters_from_format_id: true`).
- Audio formats (`type: "audio"`) have no width/height — declare `renders: [parameterizedRender({ role: "primary" })]` and encode duration/codec in `format_id.parameters` (declared via `accepts_parameters`).

#### `get_creative_delivery`

Request parameters for retrieving creative delivery data with variant-level breakdowns.

**Request:**
- Optional: `account: Account Ref`, `media_buy_ids: string[]`, `creative_ids: string[]`, `start_date: string`, `end_date: string`, `max_variants: integer`, `pagination: Pagination Request`, `context: Context`

**Response (success branch):**
- Required: `currency: string`, `reporting_period: object`, `creatives: object[]`
- Optional: `account_id: string`, `media_buy_id: string`, `pagination: object`, `errors: object[]`, `context: Context`

#### `list_creatives`

Request parameters for querying creative library with filtering and pagination.

**Request:**
- Optional: `filters: Creative Filters`, `sort: object`, `pagination: Pagination Request`, `include_assignments: boolean`, `include_snapshot: boolean`, `include_items: boolean`, `include_variables: boolean`, `include_pricing: boolean`, +6 more

**Response (success branch):**
- Required: `query_summary: object`, `pagination: Pagination Response`, `creatives: object[]`
- Optional: `format_summary: object`, `status_summary: object`, `errors: object[]`, `sandbox: boolean`, `context: Context`

#### `sync_creatives`

Request parameters for syncing creative assets with upsert semantics.

**Request:**
- Required: `account: Account Ref`, `creatives: object[]`, `idempotency_key: string`
- Optional: `creative_ids: string[]`, `assignments: object[]`, `delete_missing: boolean`, `dry_run: boolean`, `validation_mode: Validation Mode`, `push_notification_config: Push Notification Config`, `context: Context`

**Response (success branch):**
- Required: `creatives: object[]`
- Optional: `dry_run: boolean`, `sandbox: boolean`, `context: Context`

#### `validate_input`

Request parameters for validating a creative manifest against canonical formats and/or specific products without committing to a render.

**Request:**
- Required: `manifest: Creative Manifest`
- Optional: `account: Account Ref`, `brand: Brand Ref`, `targets: object[]`

**Response (success branch):**
- Required: `results: object[]`

**Deep dive:**
- docs/guides/BUILD-AN-AGENT.md — building a creative agent (server-side)
- schemas/cache/latest/creative/asset-types/index.json — asset type definitions

### Signals

#### `get_signals`

Request parameters for discovering signals based on description.

**Request:**
- Optional: `discovery_mode: 'brief' | 'wholesale'`, `account: Account Ref`, `signal_spec: string`, `signal_refs: object[]`, `signal_ids: object[]`, `destinations: object[]`, `countries: string[]`, `filters: Signal Filters`, +5 more

**Response (success branch):**
- Optional: `signals: object[]`, `errors: object[]`, `incomplete: object[]`, `wholesale_feed_version: string`, `pricing_version: string`, `cache_scope: 'public' | 'account'`, `unchanged: 'true'`, `pagination: Pagination Response`, +2 more

#### `activate_signal`

Request parameters for activating a signal on a specific platform/account.

**Request:**
- Required: `signal_agent_segment_id: string`, `destinations: object[]`, `idempotency_key: string`
- Optional: `action: 'activate' | 'deactivate'`, `pricing_option_id: string`, `account: Account Ref`, `context: Context`

**Response (success branch):**
- Required: `deployments: object[]`
- Optional: `sandbox: boolean`, `context: Context`

**Deep dive:**
- docs/guides/BUILD-AN-AGENT.md — signals agent example

### Governance

#### `create_property_list`

Request parameters for creating a new property list.

**Request:**
- Required: `name: string`, `idempotency_key: string`
- Optional: `account: Account Ref`, `description: string`, `base_properties: object[]`, `filters: Property List Filters`, `brand: Brand Ref`, `context: Context`

**Response (success branch):**
- Required: `list: Property List`, `auth_token: string`
- Optional: `replayed: boolean`, `context: Context`

#### `update_property_list`

Request parameters for updating an existing property list.

**Request:**
- Required: `list_id: string`, `idempotency_key: string`
- Optional: `account: Account Ref`, `name: string`, `description: string`, `base_properties: object[]`, `filters: Property List Filters`, `brand: Brand Ref`, `webhook_url: string`, `context: Context`

**Response (success branch):**
- Required: `list: Property List`
- Optional: `replayed: boolean`, `context: Context`

#### `get_property_list`

Request parameters for retrieving a property list with resolved properties.

**Request:**
- Required: `list_id: string`
- Optional: `account: Account Ref`, `resolve: boolean`, `pagination: object`, `context: Context`

**Response (success branch):**
- Required: `list: Property List`
- Optional: `identifiers: object[]`, `pagination: Pagination Response`, `resolved_at: string`, `cache_valid_until: string`, `coverage_gaps: object`, `context: Context`

#### `list_property_lists`

Request parameters for listing property lists.

**Request:**
- Optional: `account: Account Ref`, `name_contains: string`, `pagination: Pagination Request`, `context: Context`

**Response (success branch):**
- Required: `lists: object[]`
- Optional: `pagination: Pagination Response`, `context: Context`

#### `delete_property_list`

Request parameters for deleting a property list.

**Request:**
- Required: `list_id: string`, `idempotency_key: string`
- Optional: `account: Account Ref`, `context: Context`

**Response (success branch):**
- Required: `deleted: boolean`, `list_id: string`
- Optional: `replayed: boolean`, `context: Context`

#### `create_collection_list`

Request parameters for creating a new collection list.

**Request:**
- Required: `name: string`, `idempotency_key: string`
- Optional: `account: Account Ref`, `description: string`, `base_collections: object[]`, `filters: Collection List Filters`, `brand: Brand Ref`, `context: Context`

**Response (success branch):**
- Required: `list: Collection List`, `auth_token: string`
- Optional: `replayed: boolean`, `context: Context`

#### `update_collection_list`

Request parameters for updating an existing collection list.

**Request:**
- Required: `list_id: string`, `idempotency_key: string`
- Optional: `account: Account Ref`, `name: string`, `description: string`, `base_collections: object[]`, `filters: Collection List Filters`, `brand: Brand Ref`, `webhook_url: string`, `context: Context`

**Response (success branch):**
- Required: `list: Collection List`
- Optional: `replayed: boolean`, `context: Context`

#### `get_collection_list`

Request parameters for retrieving a collection list with resolved collections.

**Request:**
- Required: `list_id: string`
- Optional: `account: Account Ref`, `resolve: boolean`, `pagination: object`, `context: Context`

**Response (success branch):**
- Required: `list: Collection List`
- Optional: `collections: object[]`, `pagination: Pagination Response`, `resolved_at: string`, `cache_valid_until: string`, `coverage_gaps: object`, `context: Context`

#### `list_collection_lists`

Request parameters for listing collection lists.

**Request:**
- Optional: `account: Account Ref`, `name_contains: string`, `pagination: Pagination Request`, `context: Context`

**Response (success branch):**
- Required: `lists: object[]`
- Optional: `pagination: Pagination Response`, `context: Context`

#### `delete_collection_list`

Request parameters for deleting a collection list.

**Request:**
- Required: `list_id: string`, `idempotency_key: string`
- Optional: `account: Account Ref`, `context: Context`

**Response (success branch):**
- Required: `deleted: boolean`, `list_id: string`
- Optional: `replayed: boolean`, `context: Context`

#### `list_content_standards`

Request parameters for listing content standards configurations.

**Request:**
- Optional: `channels: object[]`, `languages: string[]`, `countries: string[]`, `pagination: Pagination Request`, `context: Context`

**Response (success branch):**
- Required: `standards: object[]`
- Optional: `pagination: Pagination Response`, `context: Context`

#### `get_content_standards`

Request parameters for retrieving a specific standards configuration.

**Request:**
- Required: `standards_id: string`
- Optional: `context: Context`

**Response (success branch):**
- Optional: `context: Context`

#### `create_content_standards`

Request parameters for creating a new content standards configuration.

**Request:**
- Required: `scope: object`, `idempotency_key: string`
- Optional: `registry_policy_ids: string[]`, `policies: object[]`, `calibration_exemplars: object`, `context: Context`

**Response (success branch):**
- Required: `standards_id: string`
- Optional: `context: Context`

#### `update_content_standards`

Request parameters for updating an existing content standards configuration.

**Request:**
- Required: `standards_id: string`, `idempotency_key: string`
- Optional: `scope: object`, `registry_policy_ids: string[]`, `policies: object[]`, `calibration_exemplars: object`, `context: Context`

**Response (success branch):**
- Required: `success: 'true'`, `standards_id: string`
- Optional: `context: Context`

#### `calibrate_content`

Request parameters for collaborative calibration dialogue.

**Request:**
- Required: `standards_id: string`, `artifact: Artifact`, `idempotency_key: string`
- Optional: `context: Context`

**Response (success branch):**
- Required: `verdict: Binary Verdict`
- Optional: `confidence: number`, `explanation: string`, `features: object[]`, `context: Context`

#### `validate_content_delivery`

Request parameters for batch validating delivery records.

**Request:**
- Required: `standards_id: string`, `records: object[]`
- Optional: `feature_ids: string[]`, `include_passed: boolean`, `context: Context`

**Response (success branch):**
- Required: `summary: object`, `results: object[]`
- Optional: `context: Context`

#### `get_media_buy_artifacts`

Request parameters for retrieving content artifacts from a media buy.

**Request:**
- Required: `media_buy_id: string`
- Optional: `account: Account Ref`, `package_ids: string[]`, `failures_only: boolean`, `time_range: object`, `pagination: object`, `context: Context`

**Response (success branch):**
- Required: `media_buy_id: string`, `artifacts: object[]`
- Optional: `collection_info: object`, `pagination: Pagination Response`, `context: Context`

#### `get_creative_features`

Request parameters for evaluating creative features from a governance agent.

**Request:**
- Required: `creative_manifest: Creative Manifest`
- Optional: `feature_ids: string[]`, `account: Account Ref`, `context: Context`

**Response (success branch):**
- Required: `results: object[]`
- Optional: `detail_url: string`, `audit_observations: object[]`, `pricing_option_id: string`, `vendor_cost: number`, `currency: string`, `consumption: Creative Consumption`, `context: Context`

#### `sync_plans`

Push campaign plans to the governance agent.

**Request:**
- Required: `idempotency_key: string`, `plans: object[]`
- Optional: `context: Context`

**Response (success branch):**
- Required: `plans: object[]`
- Optional: `replayed: boolean`, `context: Context`

#### `report_plan_outcome`

Report the outcome of an action to the governance agent.

**Request:**
- Required: `plan_id: string`, `idempotency_key: string`, `outcome: Outcome Type`, `governance_context: string`
- Optional: `check_id: string`, `purchase_type: Purchase Type`, `seller_response: object`, `delivery: object`, `error: object`, `context: Context`

**Response (success branch):**
- Required: `outcome_id: string`, `outcome_state: 'accepted' | 'findings'`
- Optional: `committed_budget: number`, `findings: object[]`, `plan_summary: object`, `replayed: boolean`, `context: Context`

#### `get_plan_audit_logs`

Retrieve governance state and audit trail for a plan.

**Request:**
- Optional: `plan_ids: string[]`, `portfolio_plan_ids: string[]`, `governance_contexts: string[]`, `purchase_types: object[]`, `include_entries: boolean`, `context: Context`

**Response (success branch):**
- Required: `plans: object[]`
- Optional: `context: Context`

#### `check_governance`

Orchestrator or seller calls the governance agent to validate an action against the campaign plan.

**Request:**
- Required: `plan_id: string`, `caller: string`
- Optional: `purchase_type: Purchase Type`, `tool: string`, `payload: object`, `governance_context: string`, `phase: Governance Phase`, `planned_delivery: Planned Delivery`, `delivery_metrics: object`, `modification_summary: string`, +2 more

**Response (success branch):**
- Required: `check_id: string`, `verdict: Governance Decision`, `plan_id: string`, `explanation: string`
- Optional: `findings: object[]`, `conditions: object[]`, `expires_at: string`, `next_check: string`, `categories_evaluated: string[]`, `policies_evaluated: string[]`, `mode: Governance Mode`, `governance_context: string`, +1 more

**Deep dive:**
- docs/guides/HANDLER-PATTERNS-GUIDE.md — input handler patterns for governance flows

### Sponsored Intelligence

#### `si_get_offering`

Get offering details, availability, and optionally matching products before session handoff.

**Request:**
- Required: `offering_id: string`
- Optional: `intent: string`, `context: Context`, `include_products: boolean`, `product_limit: integer`

**Response (success branch):**
- Required: `available: boolean`
- Optional: `offering_token: string`, `ttl_seconds: integer`, `checked_at: string`, `offering: object`, `matching_products: object[]`, `total_matching: integer`, `unavailable_reason: string`, `alternative_offering_ids: string[]`, +2 more

#### `si_initiate_session`

Host initiates SI session with brand agent - includes context, identity, and capability negotiation.

**Request:**
- Required: `intent: string`, `identity: Si Identity`, `idempotency_key: string`
- Optional: `context: Context`, `media_buy_id: string`, `placement: string`, `offering_id: string`, `supported_capabilities: Si Capabilities`, `offering_token: string`

**Response (success branch):**
- Required: `session_id: string`, `session_status: Si Session Status`
- Optional: `response: object`, `negotiated_capabilities: Si Capabilities`, `session_ttl_seconds: integer`, `errors: object[]`, `context: Context`

#### `si_send_message`

Send a message within an active SI session.

**Request:**
- Required: `idempotency_key: string`, `session_id: string`
- Optional: `message: string`, `action_response: object`, `context: Context`

**Response (success branch):**
- Required: `session_id: string`, `session_status: Si Session Status`
- Optional: `response: object`, `mcp_resource_uri: string`, `handoff: object`, `errors: object[]`, `context: Context`

#### `si_terminate_session`

Terminate an SI session with reason (handoff_transaction, handoff_complete, user_exit, session_timeout, host_terminated).

**Request:**
- Required: `session_id: string`, `reason: 'handoff_transaction' | 'handoff_complete' | 'user_exit' | 'session_timeout' | 'host_terminated'`
- Optional: `termination_context: object`, `context: Context`

**Response (success branch):**
- Required: `session_id: string`, `terminated: boolean`
- Optional: `session_status: Si Session Status`, `acp_handoff: object`, `follow_up: object`, `errors: object[]`, `context: Context`

**Deep dive:**
- docs/guides/ASYNC-DEVELOPER-GUIDE.md — session lifecycle patterns

### Trusted Match (TMP)

Real-time execution layer. These are HTTP operations, not MCP tools.

#### `context_match`

Evaluate available packages against content context.

#### `identity_match`

Evaluate user eligibility for packages using an opaque identity token.

## Common Flows

These are the standard tool call sequences from the AdCP storyboards. Each flow shows the tools called in order.

### Brand

**Brand baseline** — Baseline protocol storyboard — every brand agent must declare the brand protocol in capabilities and return a schema-valid brand identity.
Flow: `get_adcp_capabilities → get_brand_identity`

**Distributed brand.json mutual assertion resolves identity and relationship trust** — Consumer-under-test storyboard for AdCP 3.1 distributed brand.json: a house portfolio points at a child Brand Canonical Document, the child points back with house_domain, mutual assertion unlocks relationship trust, one-sided claims do not, identity stays brand-authored, compliance merges strictest-of, managed_by is directory metadata, and typed trademarks validate at the static-file layer.
Flow: `comply_test_controller`

**Partners MUST NOT extend trust on a single signed verify_brand_claim response** — Red conformance test for the asymmetric trust model on verify_brand_claim. A partner that auto-provisions, propagates governance, or otherwise extends relationship trust on the strength of one signed `owned` response fails — assertion direction requires reciprocation.
Flow: `comply_test_controller → verify_brand_claim → comply_test_controller → verify_brand_claim → comply_test_controller → verify_brand_claim`

**Brand agent rejects rights acquisition when governance denies** — Verifies that a brand agent propagates GOVERNANCE_DENIED when the buyer's governance plan denies a rights license.
Flow: `sync_plans → sync_accounts → sync_governance → get_rights → acquire_rights`

### Creative

**Creative lifecycle** — Baseline creative lifecycle on a stateful platform: sync display creatives, list with filtering, and preview renderings.
Flow: `get_adcp_capabilities → list_creative_formats → sync_creatives → list_creatives → preview_creative`

**Creative report_usage rejects out-of-band billing** — Verifies that a creative agent declaring bills_through_adcp: false rejects report_usage records with accepted: 0 and BILLING_OUT_OF_BAND.
Flow: `get_adcp_capabilities → report_usage`

**Creative lifecycle webhooks** — Registers account-level creative lifecycle notifications, forces a seller-side status transition, observes creative.status_changed, and verifies list_creatives snapshot repair plus optional creative.purged coverage.
Flow: `sync_accounts → sync_creatives → comply_test_controller → expect_webhook → list_creatives → comply_test_controller → expect_webhook → list_creatives`

**Native in-feed creative lifecycle** — End-to-end native_in_feed conformance: format discovery, full 12-slot asset bundle submission with pixel trackers, per-constraint validation rejection paths, and feed-rendered preview.
Flow: `get_adcp_capabilities → list_creative_formats → sync_creatives → preview_creative`

**Sales agent with creative capabilities** — Stateful sales agent that accepts pushed creative assets and renders them in its environment.
Flow: `get_adcp_capabilities → list_creative_formats → sync_creatives → preview_creative`

**Seller accepts a provenance carve-out claim surfaced as an audit observation** — Buyer submits AI-assisted creatives with human_oversight directed/edited and disclosure.required false. Seller invokes an on-list verifier, records OVERSIGHT_DISCLOSURE_CARVEOUT_CLAIMED audit observations, and accepts the creatives rather than treating the observations as rejections.
Flow: `get_products → sync_creatives → comply_test_controller`

**Seller enforces provenance_requirements on sync_creatives** — Seller publishes provenance_requirements + accepted_verifiers on a product. Four structural rejections (no provenance, missing digital_source_type, off-list verifier, missing disclosure), then a corrected resubmission with on-list verifier is accepted.
Flow: `get_products → sync_creatives`

**Seller refutes a buyer's provenance claim via on-list verifier** — Buyer attaches a digital_source_type claim. Seller invokes get_creative_features against an on-list verifier (creative_policy.accepted_verifiers); when the verifier contradicts the claim, seller rejects with PROVENANCE_CLAIM_CONTRADICTED carrying audit-safe error.details.
Flow: `get_products → sync_creatives`

**Creative ad server** — Stateful ad server with pre-loaded creatives. Generates serving tags per media buy. Optionally bills through AdCP.
Flow: `get_adcp_capabilities → list_creatives → list_creative_formats → build_creative → get_creative_delivery → report_usage`

**Creative template and transformation agent** — Stateless creative agent that takes assets in, applies templates, and produces tags or rendered output.
Flow: `get_adcp_capabilities → list_creative_formats → preview_creative → build_creative`

**Canonical format validate_input** — Validates 3.1 canonical-format dry-run semantics: structural pass/fail across canonical slots and unvalidatable_nondeterministic for seeded products.
Flow: `validate_input`

### Campaign Governance

**Governance denial and human escalation** — Buyer's governance agent denies a media buy that exceeds spending authority, escalates to a human who approves with conditions.
Flow: `get_adcp_capabilities → sync_accounts → sync_governance → sync_plans → get_products → check_governance → create_media_buy → report_plan_outcome → get_plan_audit_logs`

**Campaign governance — delivery monitoring with drift detection** — Governance agent monitors delivery, detects budget drift past thresholds, and triggers re-evaluation.
Flow: `get_adcp_capabilities → sync_plans → check_governance → create_media_buy → get_media_buy_delivery → check_governance`

**Campaign governance — denied** — Governance agent denies a media buy that exceeds the agent's spending authority. No human escalation — the buy is blocked.
Flow: `get_adcp_capabilities → sync_plans → check_governance`

**Campaign governance — conditional approval** — Governance agent approves a media buy with conditions. Buyer re-checks after meeting the conditions.
Flow: `get_adcp_capabilities → sync_plans → check_governance → create_media_buy`

### Media Buy

**Media buy seller agent** — Seller agent that receives briefs, returns products, accepts media buys, and reports delivery.
Flow: `get_adcp_capabilities → sync_accounts → sync_governance → get_products → list_creative_formats → create_media_buy → get_media_buys → list_creative_formats → sync_creatives → get_media_buy_delivery`

**Seller fulfills a media buy with audience targeting from a synced CRM audience** — Verifies that a seller advertising audience_targeting can ingest a CRM audience via sync_audiences, accept a media buy whose targeting_overlay references the bound audience_id, reject malformed audience references, and report delivery for the audience-targeted buy. Sibling to media_buy_seller/performance_buy_flow on the audience side: the unbound-id rejection is the discriminating assertion.
Flow: `sync_accounts → get_products → sync_audiences → create_media_buy → comply_test_controller → get_media_buy_delivery`

**Seller carries product allowed actions into per-buy available actions** — Validates the AdCP 3.1 media-buy action-discovery flow: product allowed_actions[], buy available_actions[], SLAWindow duration fields, self-serve mutation, and ACTION_NOT_ALLOWED enforcement.
Flow: `get_products → create_media_buy → get_media_buys → update_media_buy`

**Seller fulfills a media buy with a clicks optimization goal (target CPC)** — Verifies that a seller advertising clicks in supported_optimization_metrics can accept a media buy whose metric-kind optimization_goal targets cost-per-click and reports clicks + cost_per_click on delivery. Lightweight sibling to the performance scenarios on the click-optimization side: sellers that don't advertise clicks as an optimization metric (rare — most do; pure brand sellers like broadcast TV upper-funnel video are the not_applicable population) grade not_applicable.
Flow: `sync_accounts → get_products → create_media_buy → comply_test_controller → get_media_buy_delivery`

**Seller fulfills a video media buy with a completed_views optimization goal (target CPCV)** — Verifies that a seller advertising completed_views in supported_optimization_metrics can accept a media buy whose metric-kind optimization_goal targets completed views at a buyer-supplied view_duration_seconds, reject view_duration_seconds values not in the product's metric_optimization.supported_view_durations, and report completed_views + completion_rate on delivery. Sibling to reach_buy_flow on the video-completion side: sellers without video inventory (display-only DSPs, retail-media networks, audio-only sellers without video products) grade not_applicable.
Flow: `sync_accounts → get_products → create_media_buy → comply_test_controller → get_media_buy_delivery`

**Seller returns submitted task envelope when create_media_buy goes async** — Verifies the AdCP-payload wire shape of the submitted-arm response from create_media_buy: status='submitted', task_id present, no media_buy_id and no packages on the envelope.
Flow: `comply_test_controller → create_media_buy`

**Creative lifecycle is decoupled from media buy lifecycle** — Validates that canceling a media buy releases package-creative assignments but leaves the underlying creatives in the library with their review state intact, and that buyers can reuse released creatives on a new buy.
Flow: `get_products → create_media_buy → sync_creatives → list_creatives → update_media_buy → list_creatives → create_media_buy → sync_creatives`

**Dependency impairment end-to-end — resource transition propagates to media buy health** — Forces a creative from approved → rejected on a non-terminal media buy, verifies the buy reflects health: impaired with a matching impairments[] entry, and recovers via assignment swap to a different creative (canonical recovery vector — re-approval of the same creative_id is uncommon in production).
Flow: `get_products → create_media_buy → sync_creatives → update_media_buy → comply_test_controller → get_media_buys → comply_test_controller → get_media_buys → sync_creatives → comply_test_controller → update_media_buy → get_media_buys`

**Dependency impairment cardinality — impairments[] tracks each offline resource independently** — Two creatives on two packages of the same buy. Rejects them sequentially and verifies impairments[] grows 0 → 1 → 2; recovers them sequentially via swap-assignment and verifies impairments[] shrinks 2 → 1 → 0. Pressure-tests the inverse rule under cardinality — a seller emitting any impairment entry rather than the right one fails.
Flow: `get_products → create_media_buy → sync_creatives → update_media_buy → comply_test_controller → get_media_buys → comply_test_controller → get_media_buys → comply_test_controller → get_media_buys → sync_creatives → comply_test_controller → update_media_buy → get_media_buys → sync_creatives → comply_test_controller → update_media_buy → get_media_buys`

**Seller deduplicates the same event across multiple registered event sources** — Verifies that a seller advertising conversion_tracking.multi_source_event_dedup can register multiple event sources for the same buy and deduplicate inbound events by event_id so the same conversion from a pixel and a CAPI source counts once, not twice. Sibling to media_buy_seller/performance_buy_flow gated on the dedup sub-capability bit: sellers that don't advertise multi-source dedup grade not_applicable.
Flow: `sync_accounts → get_products → sync_event_sources → create_media_buy → log_event → comply_test_controller → get_media_buy_delivery`

**Seller honors a package-level frequency cap and reports observed frequency at-or-below the cap** — Verifies that a seller advertising frequency_capping accepts a package-level frequency_cap (max_impressions + per + window form) on create_media_buy and, after simulated delivery, reports observed reach and frequency on get_media_buy_delivery with the observed frequency at-or-below the requested cap. Runtime-enforcement scenario — no rejection arm. Sellers without frequency_capping grade not_applicable.
Flow: `sync_accounts → get_products → create_media_buy → comply_test_controller → get_media_buy_delivery`

**Seller creates buy when governance approves** — Verifies that the seller creates a media buy when governance approves the transaction.
Flow: `sync_plans → sync_accounts → sync_governance → get_products → create_media_buy`

**Seller attaches conditions when governance approves with conditions** — Verifies that the seller attaches governance conditions to the buy when governance approves with conditions.
Flow: `sync_plans → sync_accounts → sync_governance → get_products → create_media_buy`

**Seller rejects buy when governance denies** — Verifies that the seller rejects a media buy and propagates the denial when governance denies the transaction.
Flow: `sync_plans → sync_accounts → sync_governance → get_products → create_media_buy`

**Seller accepts corrected buy after governance denial** — Verifies that a buyer can recover from GOVERNANCE_DENIED by shrinking the buy to within plan limits and retrying.
Flow: `sync_plans → sync_accounts → sync_governance → get_products → create_media_buy`

**Seller rejects illegal state transitions and unknown references** — Validates that the seller returns structured AdCP errors (MEDIA_BUY_NOT_FOUND, PACKAGE_NOT_FOUND, NOT_CANCELLABLE) rather than 500s or undefined behavior when the buyer references missing entities or attempts forbidden state transitions.
Flow: `update_media_buy → get_products → create_media_buy → update_media_buy`

**Seller handles property_list / collection_list references that match zero inventory** — Verifies a seller returns a zero-forecast product or a clear error — not a crash — when a buyer references inventory lists that resolve to nothing in the seller's catalog.
Flow: `get_products → create_media_buy`

**Seller honors property_list and collection_list targeting on create and update** — Verifies that a seller accepts PropertyListReference and CollectionListReference in package targeting on create_media_buy AND update_media_buy, with parity between both paths.
Flow: `get_products → create_media_buy → get_media_buys → update_media_buy → get_media_buys`

**Seller rejects unworkable measurement_terms** — Buyer proposes measurement_terms the seller will not accept; seller returns TERMS_REJECTED; buyer retries with seller-compatible terms.
Flow: `get_products → create_media_buy`

**Creative sync unblocks pending_creatives → pending_start** — Verifies that a media buy created without creatives sits in pending_creatives until sync_creatives completes, then transitions to pending_start.
Flow: `get_products → create_media_buy → sync_creatives → update_media_buy → get_media_buys`

**Seller surfaces per-creative conversion attribution in delivery reporting** — Verifies that a seller advertising conversion_tracking.per_creative_attribution can register two creatives on a single package, accept a media buy with a CPA event-kind goal, ingest conversion events, and surface a populated by_package[].by_creative[] breakdown carrying conversions per creative in get_media_buy_delivery. Sibling to media_buy_seller/performance_buy_flow gated on the per_creative_attribution sub-capability: sellers that report attribution only at the line / package / placement / campaign granularity (retail-media, MMP-mediated mobile, CTV performance) grade not_applicable, not failing.
Flow: `sync_accounts → get_products → sync_event_sources → sync_creatives → create_media_buy → log_event → comply_test_controller → get_media_buy_delivery`

**Seller fulfills a performance (event-kind goal) media buy with a CPA target** — Verifies that a seller advertising conversion_tracking can bind an event source, accept a media buy with an event-kind optimization_goal targeting CPA, reject malformed performance briefs, ingest conversion events, and report conversion-attributed delivery including per-creative breakdown. ROAS / maximize_value goals are out of scope — see media_buy_seller/performance_buy_flow_roas (separate scenario, gated on the supported_target_kinds capability bit from #4639).
Flow: `sync_accounts → get_products → sync_event_sources → create_media_buy → log_event → comply_test_controller → get_media_buy_delivery`

**Seller fulfills a performance (event-kind goal) media buy with a ROAS target** — Verifies that a seller advertising conversion_tracking with per_ad_spend in supported_targets can accept a media buy whose event-kind optimization_goal carries a ROAS (per_ad_spend) target bound to an event source with value_field, reject ROAS goals that omit value_field on every event-source entry, ingest valued purchase events, and report conversion_value + roas alongside conversions and cost_per_acquisition. Sibling to media_buy_seller/performance_buy_flow gated on the supported_targets sub-capability: sellers that don't advertise per_ad_spend (most broadcast TV, upper-funnel video, signal-only) grade not_applicable.
Flow: `sync_accounts → get_products → sync_event_sources → create_media_buy → log_event → comply_test_controller → get_media_buy_delivery`

**Seller exposes wholesale signal options and honors package-level signal_targeting_groups** — Verifies that a seller with wholesale products and wholesale get_signals can expose signal targeting eligibility, accept package-level signal_targeting_groups with pricing, reject unknown signals, and echo the applied grouped expression on readback.
Flow: `get_products → get_signals → create_media_buy → get_media_buys → create_media_buy`

**Seller handles proposal refinement and finalize** — Verifies the full proposal lifecycle: brief with proposals, refine a proposal, finalize to committed, and accept via create_media_buy.
Flow: `sync_accounts → get_products → create_media_buy → get_products → create_media_buy`

**Seller handles proposal finalize — asap start_time form** — Variant of proposal_finalize that exercises start_time: 'asap' on create_media_buy, catching wrapper-layer rejections of the spec-defined string literal form.
Flow: `sync_accounts → get_products → create_media_buy`

**Seller returns canonical proposal error codes** — Validates that the seller returns PROPOSAL_NOT_FOUND (and PROPOSAL_EXPIRED) rather than generic NOT_FOUND or INVALID_REQUEST when the buyer references proposal IDs that do not exist or have expired.
Flow: `sync_accounts → get_products → create_media_buy`

**Seller fulfills a media buy with a reach optimization goal** — Verifies that a seller advertising reach in supported_optimization_metrics can accept a media buy whose metric-kind optimization_goal targets unique reach with a buyer-supplied reach_unit, reject reach_unit values not in the product's metric_optimization.supported_reach_units, and report reach + frequency on delivery. Sibling to the performance buy scenarios on the brand-reach side: sellers that don't advertise reach as an optimization metric (most pure performance DSPs, retail-media networks) grade not_applicable.
Flow: `sync_accounts → get_products → create_media_buy → comply_test_controller → get_media_buy_delivery → create_media_buy → comply_test_controller → get_media_buy_delivery → create_media_buy → comply_test_controller → get_media_buy_delivery → create_media_buy → comply_test_controller → get_media_buy_delivery → create_media_buy → comply_test_controller → get_media_buy_delivery`

**Seller enforces refine[] finalize-exclusivity and MULTI_FINALIZE_UNSUPPORTED** — Validates that sellers reject mixed-finalize requests (INVALID_REQUEST), structurally-invalid finalize entries, and handle multi-proposal finalize atomically or reject with MULTI_FINALIZE_UNSUPPORTED.
Flow: `sync_accounts → get_products`

**Seller handles product refinement** — Verifies that a media buy seller supports buying_mode: refine with product-level and request-level changes.
Flow: `sync_accounts → get_products`

**Vendor-metric external catalog precondition** — Exercises the 3.1 SHOULD-window behavior for vendor_metric goals whose metric_id is absent from a seeded measurement.metrics[] catalog.
Flow: `sync_accounts → comply_test_controller → create_media_buy`

**Vendor-metric optimization-goal: acceptance and rejection** — Verifies that a seller supporting vendor_metric_optimization accepts a media buy whose optimization_goal has kind 'vendor_metric' when all preconditions are met, and rejects goals that fail either the capability check or the reporting-coherence check.
Flow: `sync_accounts → get_products → create_media_buy`

**Media buy state machine lifecycle** — Validates media buy state transitions: create, pause, resume, cancel, and terminal state enforcement.
Flow: `get_adcp_capabilities → get_products → create_media_buy → update_media_buy`

**Generative seller agent** — Seller agent that generates creatives from briefs at buy time — no pre-built assets required.
Flow: `get_adcp_capabilities → sync_accounts → sync_governance → list_creative_formats → get_products → create_media_buy → sync_creatives → get_media_buy_delivery`

**Governance-aware seller** — Seller agent that composes with a campaign-governance agent on the buyer side after baseline sync_governance registration — calls check_governance before confirming spend, and propagates governance approvals, conditions, and denials unchanged. Optional claim for the full governance-check loop.
Flow: `get_adcp_capabilities`

**Seller rejects sync_governance with more than one governance agent** — Verifies that any seller required to implement sync_governance rejects a request carrying more than one governance_agents entry, enforcing the maxItems: 1 constraint.
Flow: `sync_accounts → sync_governance`

**Broadcast linear TV seller agent** — Seller agent for broadcast linear TV inventory — primetime and fringe spots with measurement windows, agency estimate numbers, Ad-ID-based creative sync, and delayed delivery reporting.
Flow: `get_adcp_capabilities → get_products → sync_governance → create_media_buy → get_media_buys → list_creative_formats → sync_creatives → expect_webhook → get_media_buy_delivery`

**Catalog-driven creative and conversion tracking** — Seller that renders dynamic ads from product catalogs, tracks conversions, and optimizes delivery based on performance feedback.
Flow: `get_adcp_capabilities → sync_accounts → sync_governance → list_creative_formats → sync_catalogs → build_creative → expect_substitution_safe → get_products → create_media_buy → sync_event_sources → log_event → provide_performance_feedback → get_media_buy_delivery`

**Guaranteed media buy with human IO approval** — Seller agent that requires human-in-the-loop IO signing before guaranteed media buys go live.
Flow: `get_adcp_capabilities → sync_accounts → sync_governance → get_products → create_media_buy → get_media_buys → sync_creatives → get_media_buy_delivery`

**Non-guaranteed auction-based media buy** — Seller agent for auction-based, non-guaranteed buying where the buyer sets bid prices and budgets.
Flow: `get_adcp_capabilities → get_products → sync_governance → create_media_buy → get_media_buys → update_media_buy → get_media_buy_delivery`

**Media buy via proposal acceptance** — Seller agent that generates curated media plan proposals the buyer can review, refine, and accept.
Flow: `get_adcp_capabilities → sync_accounts → get_products → create_media_buy → list_creative_formats → sync_creatives → get_media_buy_delivery`

**Wholesale product feed webhook registration** — Validates account-level notification_configs[] registration for agents that advertise product wholesale feed webhook events.
Flow: `sync_accounts`

**Wholesale product feed versioning** — Validates get_products wholesale feed versioning: bootstrap responses carry wholesale_feed_version/cache_scope and matching if_wholesale_feed_version probes return unchanged without product rows.
Flow: `get_products`

### Reporting

**Billing finality delivery and usage reporting** — Verifies that 3.1 billing-grade delivery rows distinguish provisional from final numbers and that report_usage accepts final usage records with finality metadata.
Flow: `sync_accounts → create_media_buy → comply_test_controller → get_media_buy_delivery → comply_test_controller → get_media_buy_delivery → report_usage`

**Seller returns valid delivery reporting** — Verifies that get_media_buy_delivery returns schema-compliant delivery data after simulated delivery via the test controller.
Flow: `sync_accounts → get_products → create_media_buy → comply_test_controller → get_media_buy_delivery → create_media_buy → comply_test_controller → get_media_buy_delivery`

**End-to-end metric accountability through the media buy lifecycle** — Buyer requires specific reporting metrics at discovery; seller filters products to those that can deliver; delivery report exposes any gaps via missing_metrics.
Flow: `sync_accounts → get_products → create_media_buy → comply_test_controller → get_media_buy_delivery`

**End-to-end vendor-metric accountability: declaration → filter → emission** — Buyer discovers products that declare a vendor metric, creates a media buy, and verifies vendor-metric values appear in the delivery report.
Flow: `sync_accounts → get_products → create_media_buy → comply_test_controller → get_media_buy_delivery`

### Products

**Canonical formats producer scenarios** — Validates that get_products can expose seeded canonical-format products through dual, v1-only, v2-only, custom, experimental, and divergent declaration shapes.
Flow: `get_products`

### Signals

**Signals baseline** — Baseline domain storyboard — every signals agent must discover signals and return an activation, regardless of whether they are owned or marketplace.
Flow: `get_adcp_capabilities → get_signals → activate_signal`

**Marketplace signal agent** — Signal agent that resells third-party data provider signals with verifiable catalog provenance.
Flow: `get_adcp_capabilities → get_signals → activate_signal`

**Signal agent rejects activation when governance denies** — Verifies that a signal agent propagates GOVERNANCE_DENIED when the buyer's governance plan denies activation.
Flow: `sync_plans → sync_accounts → sync_governance → get_signals → activate_signal`

**Owned signal agent** — Signal agent serving first-party or proprietary audience data without external catalog verification.
Flow: `get_adcp_capabilities → get_signals → activate_signal`

**get_signals pagination cursor integrity** — Validates the cursor↔has_more invariant on a paginated get_signals response by walking from a continuation page to the next page under a broad query.
Flow: `get_adcp_capabilities → get_signals`

**Wholesale signals feed webhook registration** — Validates account-level notification_configs[] registration for agents that advertise signal wholesale feed webhook events.
Flow: `sync_accounts`

**Wholesale signals feed versioning** — Validates get_signals wholesale feed versioning: bootstrap responses carry wholesale_feed_version/cache_scope and matching if_wholesale_feed_version probes return unchanged without signal rows.
Flow: `get_signals`

### Sponsored Intelligence (SI)

**Sponsored intelligence baseline** — Baseline domain storyboard — every SI agent must discover offerings, initiate a session, exchange messages, and terminate cleanly.
Flow: `get_adcp_capabilities → si_get_offering → si_initiate_session → si_send_message → si_terminate_session`

**Sponsored intelligence** — Specialism claim for agents that expose conversational sponsored experiences via the SI session lifecycle. Preview while the underlying SI tools remain `x-status: experimental`; SDK dispatch parity with other specialism IDs.

### Audiences

**Audience sync** — Full audience lifecycle: account discovery, audience creation with hashed identifiers, and audience deletion.
Flow: `get_adcp_capabilities → list_accounts → sync_audiences`

**Social platform** — Social media platform that accepts audience segments, native creatives, and conversion events from buyer agents.
Flow: `get_adcp_capabilities → sync_accounts → list_accounts → sync_governance → sync_audiences → sync_creatives → sync_catalogs → sync_creatives → sync_catalogs → sync_creatives → preview_creative → expect_substitution_safe → sync_event_sources → log_event → get_account_financials`

### Core

**Brand identity and rights licensing** — Brand agent that serves identity assets and licenses rights for AI-generated content.
Flow: `get_adcp_capabilities → get_brand_identity → get_rights → acquire_rights`

**Capability discovery** — Buyer calls get_adcp_capabilities to discover what an agent supports before making any buying or creative decisions.
Flow: `get_adcp_capabilities`

**Pagination cursor integrity — list_collection_lists** — Validates the cursor↔has_more invariant by walking a paginated list_collection_lists response from a continuation page to a terminal page.
Flow: `get_adcp_capabilities → create_collection_list → list_collection_lists`

**Pagination cursor integrity — list_content_standards** — Validates the cursor↔has_more invariant by walking a paginated list_content_standards response from a continuation page to a terminal page.
Flow: `get_adcp_capabilities → create_content_standards → list_content_standards`

**Deterministic testing** — Uses comply_test_controller to force state transitions and simulate delivery/budget, verifying state machines and reporting.
Flow: `get_adcp_capabilities → comply_test_controller → sync_accounts → list_accounts → comply_test_controller → create_media_buy → comply_test_controller → get_media_buys → comply_test_controller → sync_creatives → comply_test_controller → sync_creatives → comply_test_controller → si_initiate_session → comply_test_controller → si_send_message → create_media_buy → comply_test_controller → get_media_buy_delivery → create_media_buy → comply_test_controller`

**Pagination shape — get_media_buys** — Validates that get_media_buys responses carry a well-formed pagination envelope honoring the cursor↔has_more invariant.
Flow: `get_adcp_capabilities → get_media_buys`

**Idempotency enforcement** — Validates that mutating requests enforce idempotency_key — replays return cached responses, key reuse with a different payload returns IDEMPOTENCY_CONFLICT, fresh keys create new resources, and concurrent retries with the same key produce exactly one resource (first-insert-wins under rule 9).
Flow: `get_adcp_capabilities → create_media_buy → expect_webhook → create_media_buy → get_media_buys → expect_rate_limit_not_replayed`

**Notification config event-scope rejection** — Validates that sync_accounts.accounts[].notification_configs[] rejects media-buy-anchored notification types even though they are valid notification-type enum values.
Flow: `sync_accounts`

**Notification config lifecycle** — Validates account-level notification_configs[] lifecycle behavior on sync_accounts: paused registration, durable echo, subscriber-keyed replacement, and clear.
Flow: `sync_accounts → list_accounts → sync_accounts → list_accounts → sync_accounts → list_accounts`

**Notification config semantic rejections** — Validates account-level notification_configs[] semantic rejection for duplicate subscriber keys.
Flow: `sync_accounts`

**Pagination cursor integrity — list_creative_formats** — Validates the cursor↔has_more invariant on list_creative_formats by seeding two formats and walking pages with max_results=1.
Flow: `get_adcp_capabilities → comply_test_controller → list_creative_formats`

**Pagination continuation integrity — list_accounts** — Validates the has_more=true continuation side of list_accounts pagination by creating sandbox accounts, requesting a small first page, and following its cursor once.
Flow: `get_adcp_capabilities → sync_accounts → list_accounts`

**Pagination cursor integrity** — Validates the cursor↔has_more invariant by walking a paginated list_creatives response from a continuation page to a terminal page.
Flow: `get_adcp_capabilities → list_creatives`

**Pagination cursor integrity — list_property_lists** — Validates the cursor↔has_more invariant by walking a paginated list_property_lists response from a continuation page to a terminal page.
Flow: `get_adcp_capabilities → create_property_list → list_property_lists`

**Read-tool idempotency_key tolerance** — Validates that read-only AdCP tasks accept the 3.1 every-request idempotency_key envelope without strict wrapper rejection.
Flow: `get_adcp_capabilities → get_products → list_accounts → list_creative_formats → list_creatives → get_adcp_capabilities`

**Schema compliance — signals protocol** — Validates that signals agent responses conform to AdCP schemas with all required fields present and correctly typed.
Flow: `get_adcp_capabilities → get_signals`

**Schema compliance and temporal validation** — Validates that agent responses conform to AdCP schemas and that temporal constraints are enforced.
Flow: `get_adcp_capabilities → get_products → list_creative_formats → create_media_buy`

**v3 envelope integrity — no legacy status fields** — v3 protocol envelopes MUST NOT carry task_status or response_status — v2 legacy field names that have no semantics in v3.
Flow: `get_adcp_capabilities`

**Release-precision version negotiation** — Sellers advertise supported releases on capabilities and echo the served release on every response.
Flow: `get_adcp_capabilities`

**Webhook emission — outbound webhook conformance (signing + idempotency)** — Any agent that emits webhooks MUST carry a stable idempotency_key across retries and — when the buyer has not opted into the deprecated HMAC fallback — MUST sign deliveries under the RFC 9421 webhook profile. Graded by a runner hosting a webhook receiver during storyboard execution.
Flow: `get_adcp_capabilities → expect_webhook → expect_webhook_retry_keys_stable → fetch_brand_jwks → assert_jwks_purpose → expect_webhook_signature_valid`

**Wholesale feed bulk-change webhook registration** — Validates account-level notification_configs[] registration for agents that advertise wholesale_feed.bulk_change webhook events.
Flow: `sync_accounts`

### Governance

**Collection lists** — Curated collection lists for program-level brand safety and content targeting — create, query, update, and delete lists of content programs (shows, series, podcasts).
Flow: `get_adcp_capabilities → create_collection_list → list_collection_lists → get_collection_list → update_collection_list → delete_collection_list`

**Content standards** — Define creative quality rules, calibrate content against them, and validate that delivered ads met the standards.
Flow: `get_adcp_capabilities → create_content_standards → list_content_standards → get_content_standards → update_content_standards → calibrate_content → update_content_standards → calibrate_content → validate_content_delivery`

**Property lists** — Curated property lists for inventory grouping, targeting governance, and delivery compliance — create, query, update, delete, and validate.
Flow: `get_adcp_capabilities → create_property_list → list_property_lists → get_property_list → update_property_list → validate_property_delivery → delete_property_list`

### 

**Generative creative agent** — Agent that takes a brief and generates finished creatives from scratch — no input assets required.
Flow: `get_adcp_capabilities → list_creative_formats → build_creative → sync_catalogs → build_creative → sync_catalogs → build_creative → expect_substitution_safe`

**Runner output contract** — Required failure-detail shape that AdCP storyboard runners MUST emit so implementors can self-diagnose validation failures.

### Error Handling

**Billing-gate dispatch — sync_accounts capability vs per-buyer-agent gate** — Validates the two distinct billing-rejection paths on sync_accounts — seller-wide capability gate (BILLING_NOT_SUPPORTED) and per-buyer-agent commercial-relationship gate (BILLING_NOT_PERMITTED_FOR_AGENT) — and the clamped error.details shape that prevents the per-agent code from acting as a commercial-state oracle.
Flow: `get_adcp_capabilities → sync_accounts`

**Error handling — signals protocol** — Validates that signals agents return properly structured AdCP errors with correct codes, recovery hints, and transport bindings.
Flow: `get_adcp_capabilities → activate_signal → get_signals → activate_signal`

**Error handling and compliance** — Validates that agents return properly structured AdCP errors with correct codes, recovery hints, and transport bindings.
Flow: `get_adcp_capabilities → create_media_buy → get_products → create_media_buy → get_products → create_media_buy`

**STALE_RESPONSE advisory-success wire placement** — Validates that STALE_RESPONSE rides in errors[] on a populated success response with transport success preserved, and that STALE_RESPONSE is absent on healthy upstream responses.
Flow: `get_adcp_capabilities → comply_test_controller → get_products`

### Security

**Comply test controller — live-account denial gate** — Verifies that a seller exposing comply_test_controller refuses calls from live-mode (non-sandbox) accounts with FORBIDDEN.
Flow: `comply_test_controller`

**Authentication baseline** — Every AdCP agent MUST require authentication on protected operations. At least one of static credentials or OAuth MUST be implemented and correctly advertised.

### Security transport

**Signed requests — RFC 9421 transport-layer verification** — Agent verifies RFC 9421 HTTP Signatures on incoming AdCP requests per the transport-layer profile. Universal capability-gated storyboard — runs for any agent advertising `request_signing.supported: true` regardless of `supported_protocols`. Graded against conformance vectors covering every checklist step and canonicalization-edge rule.
Flow: `get_adcp_capabilities`

## Error Codes

Agents use the `recovery` classification to decide what to do: `transient` → retry after delay, `correctable` → fix parameters and retry, `terminal` → stop and report.

| Code | Recovery | Description |
|------|----------|-------------|
| `ACCOUNT_AMBIGUOUS` | correctable | Natural key resolves to multiple accounts. |
| `ACCOUNT_NOT_FOUND` | terminal | The account reference could not be resolved. |
| `ACCOUNT_PAYMENT_REQUIRED` | terminal | Account has an outstanding balance requiring payment before new buys. |
| `ACCOUNT_SETUP_REQUIRED` | correctable | Natural key resolved but the account needs setup before use. |
| `ACCOUNT_SUSPENDED` | terminal | Account has been suspended. |
| `ACTION_NOT_ALLOWED` | correctable | The requested mutation maps to an action that is not currently available on this media buy. Sellers MUST populate `error.details` with `attempted_action` (the `media_buy_valid_action` value the request maps to), `reason` (an `action-not-allowed-reason` value: `wrong_status`, `not_supported_on_product`, `not_supported_on_buy`, or `mode_mismatch`), and `currently_available_actions` (echo of the buy's resolved `available_actions[]` so the buyer SDK can offer recovery without a separate get_media_buys round-trip). |
| `AGENT_BLOCKED` | terminal | The calling buyer agent's commercial relationship with the seller is permanently denied — the agent is blocked. Sibling to `AGENT_SUSPENDED` on the agent-relationship axis but with no recovery path (a suspension may lift via re-onboarding; a block does not). The code itself is the discriminator — same posture as `AGENT_SUSPENDED`: no `error.details` payload, no per-agent commercial state, cross-tenant onboarding oracle clamp + channel-coverage requirements normative in error-handling.mdx Per-Agent Authorization Gate. |
| `AGENT_SUSPENDED` | terminal | The calling buyer agent's commercial relationship with the seller is temporarily paused — the agent is onboarded but currently suspended. Sibling to `ACCOUNT_SUSPENDED` (account-wide) and `CAMPAIGN_SUSPENDED` (per-plan) but scoped to the agent-relationship axis (orthogonal to any specific account on that agent). The code itself is the discriminator — it does NOT carry an `error.details` payload (mirroring `BILLING_NOT_PERMITTED_FOR_AGENT`'s discriminator-by-code pattern), and MUST NOT carry per-agent commercial state (rate cards, payment terms, credit limit, billing entity, contact channels) since full disclosure of per-agent state in a single probe is a per-agent oracle. Cross-tenant onboarding oracle clamp + channel-coverage requirements (response shape, HTTP/A2A/MCP status, headers, side effects, observability, latency parity, retry-counter side channel) are normative in error-handling.mdx Per-Agent Authorization Gate; this description does not restate them to avoid drift. |
| `AUDIENCE_TOO_SMALL` | correctable | Audience segment is below the minimum required size for targeting. |
| `AUTH_INVALID` | terminal | Credentials were presented but rejected — revoked, expired, malformed signature, or a key no longer in the seller's keystore. Sellers MUST return this code when an `Authorization` header was present but verification failed. SDK server runtime treats this code as terminal and does not refresh or retry it; use `AUTH_MISSING` / legacy `AUTH_REQUIRED` for missing request credentials that can be refreshed via `AccountStore.refreshToken`. |
| `AUTH_MISSING` | correctable | No credentials were presented. Sellers MUST return this code when no `Authorization` header was included in the request. |
| `AUTH_REQUIRED` | correctable | **Deprecated** — use `AUTH_MISSING` (no credentials presented) or `AUTH_INVALID` (credentials presented and rejected). Retained as a backward-compatible alias during the 3.x deprecation window. |
| `BILLING_NOT_PERMITTED_FOR_AGENT` | correctable | The seller's `supported_billing` capability accepts the requested model, but the calling buyer agent's commercial relationship with the seller does not — e.g., the agent is onboarded as passthrough-only (no payments relationship — only the operator can be invoiced) and `billing: 'agent'` or `billing: 'advertiser'` is rejected even though the seller supports both at the capability level. Distinct from `BILLING_NOT_SUPPORTED` (seller-wide capability) by being narrowly per-buyer-agent: the gate is the seller's onboarding record for this caller, not the seller's global wire capability. Sellers MUST emit this code only after agent identity has been established via signed-request derivation or a credential-to-agent mapping in the seller's onboarding record; callers without established identity MUST receive `BILLING_NOT_SUPPORTED` instead, to prevent the distinct code from acting as an onboarding oracle. The recovery shape is deliberately minimal — `error.details` MUST conform to `error-details/billing-not-permitted-for-agent.json` (`rejected_billing` plus an optional single `suggested_billing` retry value, typically `operator`) and 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. |
| `BILLING_NOT_SUPPORTED` | correctable | The seller declines the requested `billing` value either 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 billing relationship with the operator on this specific account). The default reject code for billing-value mismatches; `error.details` SHOULD conform to `error-details/billing-not-supported.json` (`scope` ∈ `{"capability", "account"}` plus optional `supported_billing` echo for the `"capability"` scope) so callers can dispatch without parsing prose. Distinct from `BILLING_NOT_PERMITTED_FOR_AGENT`, which is narrowly scoped to the calling buyer agent's commercial relationship with the seller (passthrough-only vs agent-billable) rather than to the seller's capability or per-account state. Sellers MUST emit `BILLING_NOT_PERMITTED_FOR_AGENT` only when agent identity has been established via signed-request derivation or a credential-to-agent mapping in the seller's onboarding record; in all other cases (unauthenticated callers and bearer credentials not mapped to a specific agent record) sellers MUST return `BILLING_NOT_SUPPORTED` and MUST omit `error.details.scope` — emitting the per-agent code or the `"account"`-scope hint without established identity is a cross-tenant onboarding oracle (same uniform-response shape required by the `*_NOT_FOUND` family). |
| `BILLING_OUT_OF_BAND` | terminal | A creative-agent billing-loop operation (`report_usage` is the canonical case) received a well-formed record that the agent will not bill on because this account bills via a non-AdCP channel — flat license, SaaS contract, bundled enterprise agreement, or any other out-of-band arrangement. The agent returns `accepted: 0` with the offending record(s) listed in `errors[]` carrying this code; the request itself is valid and silent acceptance would break buyer-side reconciliation. Distinct from `BILLING_NOT_SUPPORTED` (the seller declines a specific `billing` value on a media-buy account where AdCP billing is otherwise in scope) and `BILLING_NOT_PERMITTED_FOR_AGENT` (per-buyer-agent commercial gate on an otherwise-billable surface) by signaling that the entire billing surface is offline for this account, not that a specific value or caller is rejected. Buyers SHOULD pre-filter by reading `capabilities.creative.bills_through_adcp` from `get_adcp_capabilities` before issuing `report_usage`; agents that have not yet declared the capability remain in the probe-to-discover mode. The error is returned per-record (in the `report_usage` response `errors[]` array with `field` pointing at `usage[N]` or a specific record subpath), not at the envelope level. The code itself is the discriminator; no `error.details` shape is defined for this code (mirroring `CONFIGURATION_ERROR`'s discriminator-by-code pattern). |
| `BRAND_REQUIRED` | correctable | A billable operation was attempted without a brand reference. Every billable operation requires either a seller-assigned `account_id` or a natural key including `brand`. |
| `BUDGET_EXCEEDED` | correctable | Operation would exceed the allocated budget for the media buy or package. Distinct from BUDGET_EXHAUSTED (already spent) and BUDGET_TOO_LOW (below minimum). |
| `BUDGET_EXHAUSTED` | terminal | Account or campaign budget has been fully spent. Distinct from BUDGET_TOO_LOW (rejected at submission). |
| `BUDGET_TOO_LOW` | correctable | Budget is below the seller's minimum. |
| `CAMPAIGN_SUSPENDED` | transient | Campaign governance has been suspended pending human review; the governance agent MUST reject `check_governance` and `report_plan_outcome` calls on the affected plan until the escalation is resolved. Distinct from `ACCOUNT_SUSPENDED` (account-wide) — this is scoped to a single plan/campaign. |
| `COMPLIANCE_UNSATISFIED` | correctable | A required disclosure from the brief's compliance section cannot be satisfied by the target format — either the required position or the required persistence mode is not in the format's disclosure_capabilities. |
| `CONFIGURATION_ERROR` | terminal | The seller's deployment is misconfigured in a way that prevents handling the request — the buyer cannot fix it, retrying will not help, and reporting to the seller's operator is the only remediation. Examples: account declared with `mode: 'mock'` but no `mock_upstream_url` populated; platform declared with `mode: 'live'` or `mode: 'sandbox'` but no `upstream_url` declared; required environment variable unset on the seller process. Distinct from `INVALID_REQUEST` (buyer-fixable; the request itself is malformed), `SERVICE_UNAVAILABLE` (transient; retry-with-backoff may succeed), `UNSUPPORTED_FEATURE` (capability mismatch — the seller does not implement the requested specialism), `ACCOUNT_SETUP_REQUIRED` (buyer-side onboarding incomplete; this code is seller-side deployment incomplete), and `GOVERNANCE_UNAVAILABLE` (governance-agent-scoped; transient). Wire placement. The deployment cannot produce a success artifact, so sellers MUST flip transport-level failure markers (HTTP 5xx, MCP `isError: true`, A2A `failed`) and populate both layers per the two-layer model in `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`. The code itself is the discriminator; no `error.details` shape is defined for this code (mirroring the minimal-disclosure precedent of `AGENT_SUSPENDED` / `AGENT_BLOCKED`). Sellers SHOULD populate `error.message` with operator-actionable detail (which metadata key is missing, which env var is unset) and MUST NOT include credentials, connection strings, or stack traces — the message is wire-visible to the buyer. |
| `CONFLICT` | transient | Concurrent modification detected. The resource was modified by another request between read and write. |
| `CREATIVE_DEADLINE_EXCEEDED` | correctable | Creative change submitted after the package's creative_deadline. Distinct from CREATIVE_REJECTED (content policy failure). |
| `CREATIVE_NOT_FOUND` | correctable | Referenced creative does not exist in the agent's creative library. Sellers MUST return this code uniformly for any creative_id not owned by the calling account — never distinguish 'exists in another tenant' from 'does not exist', which would enable cross-tenant enumeration. |
| `CREATIVE_REJECTED` | correctable | Creative failed content policy review. For deadline violations, see CREATIVE_DEADLINE_EXCEEDED. |
| `CREATIVE_VALUE_NOT_ALLOWED` | correctable | A submitted text-asset value is not in the format's declared `allowed_values` list. Distinct from `CREATIVE_REJECTED` (generic content-policy failure) by being a closed-set constraint violation that the buyer can resolve mechanically without policy interpretation — the seller has published the complete list of acceptable values on the format, and any value outside that list is rejected by definition. The seller MUST set `error.field` to the offending asset's path within the manifest (e.g., `creatives[0].creative_manifest.assets[0].value` or the field name declared by the format) and SHOULD include the format's `allowed_values` array in `error.details.allowed_values` so the buyer agent can re-prompt its LLM with constrained sampling. |
| `CREDENTIAL_IN_ARGS` | terminal | The seller detected a buyer-principal credential placed in request args (top-level, in `context`, in `ext`, or any other nested location in the task payload) instead of arriving on the transport's authentication channel. Buyer-principal credentials MUST arrive on the transport's authentication channel (`Authorization: Bearer` per RFC 6750 §2 for HTTP, RFC 9421 signature headers for signed requests, MCP/A2A authentication framing per RFC 9728 §3) and MUST NOT travel inside the task payload. Distinct from `AUTH_MISSING` (no credentials presented on the transport channel) and `AUTH_INVALID` (credentials presented but rejected on the transport channel) and `PERMISSION_DENIED` (authenticated caller not authorized for the action). Distinct from the receiver-side credentials carried in `push_notification_config.authentication.credentials`, which configure the seller's webhook callback authentication and are not buyer-principal credentials — those are an explicit carve-out and MUST NOT trigger this code. Sellers SHOULD reject credential-in-args under AdCP 3.1; the requirement upgrades to MUST 90 days after the 3.1 publication date. |
| `FIELD_NOT_PERMITTED` | correctable | A request field is not in the caller's `field_scopes` allowlist for this task. Sellers declaring `field_scopes` on the account's `authorization` object MUST reject any request that sets a non-allowlisted field with this code. Distinct from `VALIDATION_ERROR` (schema/business-rule violation) - the field is valid, just not writable by this caller. `error.field` MUST identify the exact offending field path (e.g., `packages[0].budget`); when multiple fields are disallowed, sellers SHOULD return one error per field, or MAY enumerate them in `error.details.fields`. |
| `FORMAT_DECLARATION_DIVERGENT` | correctable | Non-fatal advisory raised when a product carries BOTH `format_ids` (v1) AND `format_options` (v2) and the two disagree (different canonical, different dimensions, different orientation) after projection. The producer's contract is that both shapes MUST refer to the same underlying declaration; divergence is a producer bug. Either side MAY emit this code: a SELLER may self-detect on emit (own producer bug; rare), or more commonly a consumer-SDK detects on consumption. SDKs MUST prefer `format_options` (the richer surface) when both are present and MUST surface the divergent product so it's observable rather than silently picked-one-and-dropped-other. Hard-failing the entire `get_products` response is discouraged — it punishes downstream buyers for the producer bug. **Surface placement (normative).** Same single-surface mandate as `FORMAT_PROJECTION_FAILED`: SDKs that detect this on consumption MUST augment the response's `errors[]` array with an entry carrying `source: "sdk"`, `sdk_id: "<package>@<version>"`, `code: "FORMAT_DECLARATION_DIVERGENT"`, and the field+details described below. Logger-only is insufficient; lint-output channels are NOT acceptable as the surface (the multi-hop agent network needs warnings to propagate across SDK boundaries via the wire response). `error.field` MUST point at the offending product; `error.details` SHOULD carry `{ product_id, format_ids, format_options_summary, divergence_reason }` so buyer SDKs can flag the producer for follow-up. **Multi-hop deduplication.** Each hop that detects the same divergence SHOULD deduplicate by `(code, field)` rather than re-emit; the existing entry's `sdk_id` identifies which earlier processor saw it first. |
| `FORMAT_DECLARATION_V1_AMBIGUOUS` | correctable | Non-fatal advisory raised when an SDK detects that a product's v2 declaration cannot be unambiguously projected back to a single v1 named format because the v1-canonical-mapping registry has only family-level structural entries for this canonical (no invertible `format_id_glob` literal). The family is known (e.g., 'this is a video_vast'); the specific v1 named format isn't pickable mechanically. Distinct from `FORMAT_PROJECTION_FAILED` (registry-coverage gap, correctable by adding a registry entry) — ambiguity is structural: the family is defined but a specific format can't be picked without seller assertion. Surface placement: same single-mandate as `FORMAT_PROJECTION_FAILED` and `FORMAT_DECLARATION_DIVERGENT` — SDKs MUST augment the response's `errors[]` array with an entry carrying `source: "sdk"`, `sdk_id`, `code: "FORMAT_DECLARATION_V1_AMBIGUOUS"`, `field` pointing at the offending declaration, and `error.details` SHOULD carry `{ format_kind, registry_matches: [<list of structural entries that matched at family level>], product_id }` so adopters can see why the inversion was ambiguous. **SDKs MUST NOT synthesize a v1_format_ref** in this case (or any other case). v1↔v2 explicit pairing is seller-asserted only — SDKs encountering family-only registry matches MUST treat the v2 declaration as v1-unreachable and surface this code rather than invent a plausible v1 format_id. The seller's path: author `v1_format_ref` on the v2 declaration to disambiguate (the authoritative pairing per `v1-canonical-mapping.json` resolution step 1), or accept that v1-only buyers won't see this product. |
| `FORMAT_DECLARATION_V1_LOSSY_MULTI_SIZE` | correctable | Non-fatal advisory raised when a v2 declaration carries `params.sizes[]` with N entries but only M v1_format_ref entries (M < N). The seller has asserted some v1 named formats but not enough to cover all declared sizes — v1-only buyers see partial coverage. Emitted **alongside** the partial v1 emission (NOT in place of it): the product still appears on the v1 wire under the M sizes the seller covered; this code tells v1-aware downstream agents that N-M sizes were dropped from the projection. Surface placement: SDKs that detect on emission OR consumption MUST augment the response's `errors[]` with `source: "sdk"` (or `"producer"` if the seller self-detects on emit), `sdk_id`, `code: "FORMAT_DECLARATION_V1_LOSSY_MULTI_SIZE"`, `field` pointing at the offending declaration, and `error.details` SHOULD carry `{ product_id, declared_sizes: [{w,h}, …], covered_sizes: [{w,h}, …], dropped_sizes: [{w,h}, …] }` so buyer agents see which sizes were lost. **SDKs MAY (non-normative) fan out automatically** by catalog lookup — for each entry in `sizes[]` lacking a corresponding `v1_format_ref`, the SDK consults the AAO catalog for the per-size v1 named format (e.g., for `{width: 728, height: 90}` look up `display_728x90_image`) and emits it under `format_ids[]`. This is opt-in (requires catalog access); when SDKs fan out, they SHOULD still emit this code as a transparency advisory so downstream consumers know the v1 emit was synthesized rather than seller-asserted. Recovery: warning — non-fatal, no retry. Seller fix: add `v1_format_ref[]` entries for the missing sizes. |
| `FORMAT_OPTION_UNRESOLVED` | correctable | Non-fatal advisory raised when a placement in `adagents.json` (or any consumer of `placement-definition.json`) carries `format_options[].format_option_id` referencing a `format_option_id` that does NOT exist in the file's top-level `formats[]`. The reference is broken — the publisher's catalog claims the placement accepts a format option that isn't declared. **Resolution scope is same-file only.** Cross-file `format_option_id` lookup is not supported by design (closes off format_option_id squatting across publisher boundaries — a malicious file cannot reference another publisher's format_option_id and claim its narrowing). Buyer SDKs MUST fail closed for the placement (drop the format from the placement's accepted format set) and MUST surface this code rather than silently dropping or guessing what the publisher meant. Surface placement: same single-mandate as the other FORMAT_* codes — SDKs that detect on consumption MUST augment the response's `errors[]` with `source: "sdk"`, `sdk_id`, `code: "FORMAT_OPTION_UNRESOLVED"`, `field` pointing at the offending placement (e.g., `placements[2].format_options[1].format_option_id`), and `error.details` SHOULD carry `{ placement_id, format_option_id, declared_format_options: [<list of format_option_ids actually in formats[]>] }` so the publisher can fix. |
| `FORMAT_PROJECTION_FAILED` | correctable | Non-fatal advisory raised when a legacy named format on a product cannot be projected to a canonical-formats `ProductFormatDeclaration` via the resolution order in `v1-canonical-mapping.json` (explicit `canonical` field → format_id_glob → structural match → fail-closed). The product is still valid on the legacy named-format path; only the 3.1+ `format_options` projection failed. Primarily a **consumer-SDK concern** — the seller didn't fail; the consumer-side SDK couldn't project on their behalf. `error.field` MUST point at the offending product (e.g., `products[3].format_ids[0]`); `error.details` SHOULD carry `{ format_id, product_id, resolution_failure: "no_explicit_canonical" | "no_registry_match" | "no_structural_match" }` so buyer SDKs can route remediation (suggest the seller add an explicit `canonical` field, or file a registry PR). **Surface placement (normative).** SDKs that detect this on consumption MUST augment the response's `errors[]` array with an entry carrying `source: "sdk"`, `sdk_id: "<package>@<version>"`, `code: "FORMAT_PROJECTION_FAILED"`, and the field+details described above. This is the single mandated surface — logger-only is insufficient and a separate lint-output channel is NOT acceptable (AdCP is a multi-hop agent network; warnings need to propagate across hops or each hop has to re-detect locally). Sellers MAY emit this code on their own response when they self-detect a non-projectable format on emit; producer-emitted entries omit `source` (or set `source: "producer"`). The response stays 200/success regardless of who emits; this is non-fatal. **Multi-hop deduplication.** Each hop that detects the same condition SHOULD deduplicate by `(code, field)` rather than re-emit. The existing entry's `sdk_id` identifies which earlier processor saw it first; downstream SDKs SHOULD NOT add a second entry for the same `(code, field)` pair unless they have materially different `error.details` (e.g., a different `resolution_failure` reason from a different registry version). See canonical-formats.mdx 'Dual emission and v2↔v1 projection' for the full rules. |
| `GOVERNANCE_DENIED` | correctable | A registered governance agent denied the transaction. Sellers MUST place the denial in the task's structured rejection arm when one exists (e.g., `acquire_rights` → `AcquireRightsRejected`, `creative_approval` → `CreativeRejected`); otherwise in `errors[]` + `adcp_error`. Buyers MUST dispatch on the response's discriminated `status` first and fall back to `errors[].code` / `adcp_error.code` only when no rejection arm exists for that task. The buyer may restructure the buy (e.g., reduce budget, split into smaller transactions), escalate to human spending authority, or contact the governance agent for details. Wire placement (full guidance). Governance denial is a structured business outcome, not a system error — the governance call SUCCEEDED and the agent returned a denial verdict. Two cases: 1. Task response defines a structured rejection arm. The arm IS the canonical denial shape. The seller populates `reason` (human-readable, propagating governance findings) and `suggestions` (optional) and does NOT additionally emit `GOVERNANCE_DENIED` in `errors[]` or `adcp_error`. The rejection arms enforce this at the schema layer: e.g., `AcquireRightsRejected` and `CreativeRejected` both declare `not: { required: [errors] }`, so dual-emission is already a schema violation. The code does not appear on the wire when the rejection arm is used. Transport-level success markers MUST NOT be flipped (HTTP 200, MCP `isError: false`, A2A `succeeded`) — the task ran successfully and produced a structured response. 2. Task response has no rejection arm (e.g., `create_media_buy` returns Success / Error / Submitted arms only). The seller populates `errors[].code: GOVERNANCE_DENIED` in the payload AND `adcp_error.code: GOVERNANCE_DENIED` on the envelope per the two-layer model in `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`. Transport-level failure markers DO flip in this case (HTTP 4xx, MCP `isError: true`, A2A `failed`) — the task could not produce a success artifact. The rule generalizes to any current or future task whose response defines a discriminated rejection arm. In either placement, sellers SHOULD propagate governance findings verbatim — buyers' recovery decisions depend on what specifically was rejected. `GOVERNANCE_DENIED` is reserved for verdicts received from a reachable governance agent; if the governance call itself failed (timeout, network, config error), use `GOVERNANCE_UNAVAILABLE` instead. |
| `GOVERNANCE_UNAVAILABLE` | transient | A registered governance agent is unreachable. Sellers MUST place this code in `errors[]` + `adcp_error` (never a structured rejection arm) and flip transport-level failure markers (HTTP 5xx, MCP `isError: true`, A2A `failed`). Distinct from `GOVERNANCE_DENIED` (agent reachable and explicitly denied — see that code's wire-placement guidance). Wire placement (full guidance). Governance unavailability is a system error — the governance call FAILED (timeout, network, config error) and the seller could not get a verdict at all. Always populate both layers per the two-layer model in `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`. Do NOT use a structured rejection arm for unavailability even when the task offers one — the buyer's recovery semantics differ (retry-with-backoff for unavailability vs. restructure-or-escalate for denial), and conflating them masks the system-error signal. |
| `IDEMPOTENCY_CONFLICT` | correctable | An earlier request with the same idempotency_key was processed with a different canonical payload within the seller's replay window. Distinct from CONFLICT (concurrent write) — this indicates the client reused a key across semantically different requests. |
| `IDEMPOTENCY_EXPIRED` | correctable | The idempotency_key was seen previously but its cached response has been evicted because it is past the seller's declared replay_ttl_seconds. Distinct from IDEMPOTENCY_CONFLICT (different payload within window) — this indicates the retry arrived too late for at-most-once guarantees. If the buyer has any evidence the prior call succeeded (partial response received before crash, entry in the buyer's own DB, a webhook fired), the buyer MUST do the natural-key check BEFORE minting a new key — minting a new key in that situation is exactly how double-creation happens. |
| `IDEMPOTENCY_IN_FLIGHT` | transient | A prior request with the same `idempotency_key` is still being processed and has not yet produced a cached response. The second request arrived before the first completed. Sellers MAY return this code instead of blocking the second caller until the first finishes — useful when the first call invokes a slow downstream system (SSP, ad server, payment provider). Distinct from IDEMPOTENCY_CONFLICT (different canonical payload — a client bug) and from CONFLICT (concurrent modification of a different resource) — IDEMPOTENCY_IN_FLIGHT is the seller telling the buyer 'your retry was correct but your previous attempt is still running, come back shortly.' Sellers SHOULD populate `error.details.retry_after` (seconds, integer) with a wait hint based on the first request's elapsed time and expected completion. Buyers MUST treat this as transient and MUST NOT mint a fresh `idempotency_key` — minting a new key turns a safe retry into a double-execution race. |
| `INVALID_REQUEST` | correctable | Request is malformed, missing required fields, or violates schema constraints. |
| `INVALID_STATE` | correctable | Operation is not permitted for the resource's current status (e.g., updating a completed or canceled media buy, or modifying a canceled package). |
| `IO_REQUIRED` | correctable | The committed proposal requires a signed insertion order but no io_acceptance was provided. |
| `MEDIA_BUY_NOT_FOUND` | correctable | Referenced media buy does not exist or is not accessible to the requesting agent. |
| `MULTI_FINALIZE_UNSUPPORTED` | correctable | Returned by sellers that cannot guarantee atomic commit across multiple proposals in a single `get_products` call (multiple `action: 'finalize'` entries in `refine[]` targeting different `proposal_id` values). The buyer's intent — atomic multi-proposal finalize — is structurally well-formed and per spec atomic, but this seller's downstream stack cannot satisfy the atomicity guarantee (e.g., the proposals route to two different ad servers with no 2PC). More specific than `INVALID_REQUEST` so buyers can distinguish 'this seller doesn't support multi-finalize' from 'the request itself is malformed'. See [refinement guide § Finalize is exclusive](/docs/media-buy/product-discovery/refinement#finalize-is-exclusive-within-refine). |
| `NOT_CANCELLABLE` | correctable | The media buy or package cannot be canceled in its current state. The seller may have contractual or operational constraints that prevent cancellation. |
| `PACKAGE_NOT_FOUND` | correctable | Referenced package does not exist within the specified media buy. |
| `PAYMENT_TERMS_NOT_SUPPORTED` | correctable | The seller does not accept the requested `payment_terms` value for this account. Payment terms are never silently remapped — sellers either accept or reject. Distinct from `BILLING_NOT_SUPPORTED` (the `billing` enum) by being narrowly about the `payment_terms` enum on the same account. |
| `PERMISSION_DENIED` | correctable | The authenticated caller is not authorized for the requested action under the seller's own policies, or a required signed credential (e.g., a `governance_context` token on a spend-commit) is missing, fails verification, or was issued for a different plan, seller, or phase. Distinct from `AUTH_MISSING` (no credentials presented), `AUTH_INVALID` (credentials presented but rejected), `GOVERNANCE_DENIED` (governance agent denied), `AGENT_SUSPENDED` (agent's relationship temporarily paused), and `AGENT_BLOCKED` (agent's relationship permanently denied). When the gate that fired is specifically a non-status per-agent provisioning constraint — e.g., the agent is provisioned for sandbox traffic only and the request was against a non-sandbox account — `error.details` SHOULD conform to `error-details/agent-permission-denied.json` (`scope: "agent"` plus `reason: "sandbox_only"`) so callers can dispatch without parsing prose. Sellers MUST emit `scope: "agent"` only when buyer-agent identity has been established via signed-request derivation or 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 `PERMISSION_DENIED` and MUST omit `error.details.scope` — emitting the per-agent scope without established identity is a cross-tenant onboarding oracle, and the omit MUST be enforced across every observable channel (response shape, HTTP/A2A/MCP status, headers, side effects, observability, latency parity) per the channel-coverage rules in error-handling.mdx Per-Agent Authorization Gate, mirroring the `*_NOT_FOUND` uniform-response rule and `BILLING_NOT_PERMITTED_FOR_AGENT`. The `suspended` and `blocked` per-agent states are NOT carried on this code — sellers MUST emit `AGENT_SUSPENDED` / `AGENT_BLOCKED` instead, each of which is its own discriminator. |
| `PIXEL_TRACKER_LOSSY_DOWNGRADE` | correctable | Non-fatal advisory raised when a 3.1 buyer SDK downgrades a `pixel_tracker` asset to the v1 `{asset_type: url, url_type: tracker_pixel}` shape for a 3.0.x seller that doesn't recognize the new asset type. The URL is still emitted on the wire and the seller will fire it as a tracker pixel; what's lost is the event/method discrimination. Downgrade rules (normative):
- `event: impression` + `method: img` → no loss; emit as `{asset_type: url, url_type: tracker_pixel, url, asset_id: impression_tracker}`
- `event: viewable_mrc_50` / `viewable_mrc_100` / `viewable_video_50` / `audible_video_complete` → emit with `asset_id: viewability_tracker`; advisory `lost_event: <variant>` (specific viewability variant collapses to a single v1 slot)
- `event: click` → emit with `asset_id: click_tracker`; no meaningful loss
- `event: custom, custom_event_name: X` → emit with `asset_id: impression_tracker` (default tracker_pixel fires on impression); advisory `lost_event: "custom"`, `lost_custom_event_name: X` (custom event timing collapses to impression timing)
- `method: js` → emit unchanged shape (url, url_type:tracker_pixel); advisory `lost_method: "js"` (v1 seller will fire as HTTP GET; the URL is hit and any counter-based measurement increments, but the response body won't execute as JS — measurement that depends on JS execution, e.g., OMID-style verification, viewability observers, cross-domain cookie setters, won't work. Simple counter pixels still work.) Surface: SDK that performs the downgrade MUST augment the response's `errors[]` with `source: "sdk"`, `sdk_id`, `code: "PIXEL_TRACKER_LOSSY_DOWNGRADE"`, `field` pointing at the affected manifest asset path, and `error.details` SHOULD carry `{ asset_id, original_event, original_method, original_custom_event_name (if present), downgrade_target: "url+tracker_pixel", lost_fields: [<list>] }`. One advisory per downgraded asset; SDKs SHOULD NOT collapse multiple downgrades into a single advisory entry — per-asset details let the buyer's measurement-plan owner decide whether each loss is tolerable. Recovery: warning — non-fatal, no retry. Buyer-side decision: accept the loss (most simple counter pixels survive), or fail the buy and route to a 3.1-capable seller. Seller-side fix: upgrade to 3.1 and accept `pixel_tracker` natively. |
| `PIXEL_TRACKER_UPGRADE_INFERRED` | correctable | Non-fatal advisory raised when a 3.1 buyer SDK upgrades a v1 `{asset_type: url, url_type: tracker_pixel}` to a `pixel_tracker` asset by INFERRING the event and method from the v1 asset_id and conventional defaults. The inference is structural — the SDK doesn't have explicit event/method values, only the v1 asset_id hint and `url_type: tracker_pixel` (which implies `method: img` by default). Inference rules (normative):
- `asset_id: impression_tracker` → `event: impression, method: img`
- `asset_id: viewability_tracker` → `event: viewable_mrc_50, method: img` (50% is the most common default; specific viewability variant cannot be recovered from v1 shape)
- `asset_id: click_tracker` → `event: click, method: img`
- `asset_id: <other>` → `event: custom, custom_event_name: <original asset_id>, method: img` Surface: SDK MUST augment the response's `errors[]` with `code: "PIXEL_TRACKER_UPGRADE_INFERRED"`, `field` pointing at the upgraded asset path, and `error.details` SHOULD carry `{ asset_id, inferred_event, inferred_method, inference_basis: "asset_id_convention" | "default" }`. Buyer agents reading the response can re-prompt the seller for explicit values if precise measurement matters. Recovery: warning — non-fatal, no retry. Seller-side: upgrade emit path to ship pixel_tracker shape directly when 3.1-capable; until then, conventional asset_id values give the SDK enough signal to upgrade without losing critical semantics. |
| `PLAN_NOT_FOUND` | correctable | Referenced governance plan does not exist or is not accessible to the requesting agent. Sellers MUST return this code uniformly for any plan_id not accessible to the calling account — never distinguish 'exists but unauthorized' from 'does not exist', which would enable cross-tenant enumeration of governance plans. |
| `POLICY_VIOLATION` | correctable | Request violates the seller's content or advertising policies. |
| `PRIVATE_FIELD_IN_PUBLIC_PLACEMENT` | correctable | Fatal producer-side error raised when a public placement object (`Product.placements[]` in `get_products` or `placements[]` in adagents.json) exposes seller-private operational fields such as `visibility`, `source`, `origin`, or `delivery_mappings`. This is a private-data leak, not an ordinary syntactic mismatch. Consumers that detect it MUST fail closed for that placement and surface this code so monitoring can alarm on the leak specifically instead of burying it under generic schema validation. `error.field` SHOULD point at the offending placement path and `error.details` SHOULD carry `{ placement_id, leaked_fields: [<field names>] }` without echoing private field values. |
| `PRODUCT_EXPIRED` | correctable | One or more referenced products have passed their expires_at timestamp and are no longer available for purchase. |
| `PRODUCT_NOT_FOUND` | correctable | One or more referenced product IDs are unknown or expired. |
| `PRODUCT_UNAVAILABLE` | correctable | The requested product is sold out or no longer available. |
| `PROPOSAL_EXPIRED` | correctable | A referenced proposal ID has passed its expires_at timestamp. |
| `PROPOSAL_NOT_COMMITTED` | correctable | The referenced proposal has proposal_status 'draft' and cannot be used to create a media buy. |
| `PROPOSAL_NOT_FOUND` | correctable | The referenced proposal_id is not recognized by the seller — never finalized, belongs to a different tenant, or evicted from the seller's session cache before consumption. Distinct from `PROPOSAL_EXPIRED` (a known proposal whose `expires_at` window has passed) and `PROPOSAL_NOT_COMMITTED` (a known proposal still in `draft`). |
| `PROVENANCE_CLAIM_CONTRADICTED` | correctable | Seller invoked a governance agent from `creative_policy.accepted_verifiers` via `get_creative_features` and the verifier's result contradicts the buyer's provenance claim - e.g., buyer claims `digital_source_type: digital_capture` but the AI-detection feature returns `ai_generated: true` above the seller's confidence threshold. Distinct from the `PROVENANCE_*_MISSING` family (structural absence) by being an active refutation. `error.details` SHOULD be limited to the audit-safe allowlist `{ agent_url, feature_id, claimed_value, observed_value, confidence }`; sellers MUST NOT forward arbitrary verifier extension fields, `detail_url`, or any verifier response shape that may carry cross-tenant or PII data. When the seller calls a different on-list agent than the buyer nominated (the seller is the verifier-of-record), `error.details.agent_url` is the agent the seller actually called and `error.details.substituted_for` SHOULD carry the buyer's nominated `agent_url` so the buyer can reconcile. |
| `PROVENANCE_DIGITAL_SOURCE_TYPE_MISSING` | correctable | Seller's `creative_policy.provenance_requirements.require_digital_source_type` is true and the submitted creative's resolved provenance (after inheritance) has no `digital_source_type` value, or has it set to null. Distinct from `PROVENANCE_REQUIRED` (no provenance object at all) - provenance is present, just missing this specific field. `error.field` MUST point at the resolved provenance path that was inspected (e.g., `creatives[0].creative_manifest.provenance.digital_source_type`). |
| `PROVENANCE_DISCLOSURE_MISSING` | correctable | Seller's `creative_policy.provenance_requirements.require_disclosure_metadata` is true and the submitted creative's resolved provenance has no `disclosure.required` boolean, or `disclosure.required` is true with no `disclosure.jurisdictions` entries. `error.field` MUST point at `provenance.disclosure` (e.g., `creatives[0].creative_manifest.provenance.disclosure`). |
| `PROVENANCE_EMBEDDED_MISSING` | correctable | Seller's `creative_policy.provenance_requirements.require_embedded_provenance` is true and the submitted creative's resolved provenance has no `embedded_provenance` array, or has it as an empty array. Used in pipelines where sidecar `c2pa.manifest_url` is stripped by intermediaries and the seller requires content-stream-resilient provenance. `error.field` MUST point at `provenance.embedded_provenance` on the resolved manifest. |
| `PROVENANCE_REQUIRED` | correctable | Seller's `creative_policy.provenance_required` is true and the submitted creative has no `provenance` object on the manifest, on the creative-asset, or on any individual asset. Distinct from `CREATIVE_REJECTED` (generic content-policy failure) by being narrowly about provenance presence. `error.field` MUST point at the path where provenance was expected (e.g., `creatives[0].creative_manifest`). |
| `PROVENANCE_VERIFIER_NOT_ACCEPTED` | correctable | Buyer attached a `verify_agent.agent_url` on `embedded_provenance[]` or `watermarks[]` that does not match (canonicalized per /docs/reference/url-canonicalization: lowercase scheme and host, strip default port, normalize path dot-segments) any entry in the seller's `creative_policy.accepted_verifiers[].agent_url`. The seller does not call buyer-asserted endpoints outside its allowlist; this is the cross-check that closes the buyer-controlled-URL trust gap. `error.field` MUST point at the offending `verify_agent.agent_url` path; `error.details` SHOULD include a reference to the product whose `creative_policy.accepted_verifiers` the buyer should consult (the buyer already has this from `get_products`). |
| `RATE_LIMITED` | transient | Request rate exceeded. Retry after the retry_after interval. |
| `READ_ONLY_SCOPE` | correctable | The caller's scope is read-only; the invoked task would mutate state and was rejected. Distinct from `SCOPE_INSUFFICIENT` (task not in scope at all) — the task is in some scopes this seller supports, just not this caller's. |
| `REFERENCE_NOT_FOUND` | correctable | Generic fallback for a referenced identifier, grant, session, or other resource that does not exist or is not accessible by the caller. Use when no resource-specific not-found code applies (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 also use REFERENCE_NOT_FOUND rather than minting a custom *_NOT_FOUND code. See 'Uniform response for inaccessible references' in error-handling.mdx for the full MUST list. Summary of the uniform-response MUST: sellers MUST return the same response for 'exists but the caller lacks access' as for 'does not exist' across every observable channel — error.code/message/field/details (message MUST be generic; error.field MUST be identical across both cases on typed parameters); HTTP status, A2A task.status.state, and MCP isError; response headers (ETag, Cache-Control, per-type rate-limit buckets, CDN tags); side effects (webhook/audit writes, background-job enqueues, per-type quota counters, DB-shard routing); and observability (logs, APM spans, third-party error telemetry like Sentry/Datadog). Sellers MUST perform the same resolution-and-authorization work on both paths (resolve-then-authorize; on true-miss still run an authorization decision of equivalent shape against an empty principal set so authorizer latency is not a side channel). Cache population MUST NOT be gated on authorization. Polymorphism is evaluated against the tool-schema's declared parameter shape before any lookup, and a tool's declared shape MUST be identical across all callers. |
| `REQUOTE_REQUIRED` | correctable | An update_media_buy request changes the parameter envelope (budget, flight dates, volume, targeting) the original quote was priced against. The pricing_option remains locked; the seller is declining the requested shape at that price. Distinct from TERMS_REJECTED (measurement) and POLICY_VIOLATION (content). Sellers SHOULD populate error.details.envelope_field with the field path(s) that breached the envelope (e.g., 'packages[0].budget', 'end_time') so the buyer's agent can autonomously re-discover. |
| `SCOPE_INSUFFICIENT` | correctable | The authenticated caller is not authorized for the invoked task — the task is not in the caller's `allowed_tasks` for this account (discoverable via the `authorization` object on sync_accounts / list_accounts responses). Distinct from `PERMISSION_DENIED` (generic authz failure, often credential-shaped) by being narrowly about task-level scope. Sellers SHOULD populate `error.details.introspection_hint` pointing at where the caller can re-read its scope (strawman: `{ task: 'list_accounts', account: {...} }`). |
| `SERVICE_UNAVAILABLE` | transient | Seller service is temporarily unavailable. Retry with exponential backoff. |
| `SESSION_NOT_FOUND` | correctable | SI session ID is invalid, expired, or does not exist. |
| `SESSION_TERMINATED` | correctable | SI session has already been terminated and cannot accept further messages. |
| `SIGNAL_NOT_FOUND` | correctable | Referenced signal does not exist in the agent's catalog. Sellers MUST return this code uniformly for any signal_ref not accessible to the calling account — never distinguish 'exists but unauthorized' from 'does not exist', which would enable cross-tenant enumeration. |
| `STALE_RESPONSE` | transient | Non-fatal advisory raised when the seller's live fetch to an upstream or sub-agent failed (timeout, connection error, downstream 5xx) and the response payload was satisfied from a cached prior result that is past the seller's freshness target for this surface. Emitted **alongside** a populated success payload — the caller's request still completes from a usable cache hit; this code tells downstream consumers that the data is older than the seller would normally serve. Distinct from `SERVICE_UNAVAILABLE` (seller's own service is down, no payload — transient, retry-with-backoff) by signalling **graceful degradation**: the seller's own service is fine, but one of its dependencies is currently unreachable and the seller chose to honor the request from cache rather than return empty. Sellers MUST emit `STALE_RESPONSE` ONLY when the response payload is non-empty AND derived from a cache entry whose `cache_age_seconds` exceeds the surface's freshness target. When no cached entry exists (or the cache hit is within freshness target), sellers MUST NOT emit this code — return the empty-or-fresh response with whatever upstream-failure code applies (e.g., `SERVICE_UNAVAILABLE`). **Wire placement (normative).** Transport-level success markers stay flipped to success (HTTP 200, MCP `isError: false`, A2A `succeeded`) — the task ran successfully and produced a response, even if from cache. The advisory rides in `errors[]` on the payload and MUST NOT be promoted to `adcp_error` on the envelope (envelope-level errors are reserved for the empty-payload failure case per the two-layer model in `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`). `error.field` SHOULD point at the affected payload path (e.g., `formats` for `list_creative_formats`, `products` for `get_products`). `error.details` SHOULD conform to `error-details/stale-response.json` — `served_from_cache` (required, always `true`), `cache_age_seconds` (required), and optionally `freshness_target_seconds`, `upstream` (the dependency that failed), and `original_error` (the underlying failure code/message). **Multiple stale upstreams.** When N sub-agents are stale (e.g., a `list_creative_formats` registry aggregating from multiple creative agents), the seller SHOULD emit **one `STALE_RESPONSE` entry per affected upstream** rather than aggregating — the per-upstream shape mirrors the existing precedent set by `PIXEL_TRACKER_LOSSY_DOWNGRADE` (one advisory per downgraded asset) and lets buyer agents reason about which sub-population of the payload is stale. Each entry's `error.field` SHOULD narrow to the affected slice (e.g., `formats` for formats sourced from the stale upstream). |
| `TERMS_REJECTED` | correctable | Buyer-proposed measurement_terms were rejected by the seller. The error details SHOULD identify which specific term was rejected and the seller's acceptable range or supported vendors. |
| `UNSUPPORTED_FEATURE` | correctable | A requested feature or field is not supported by this seller. |
| `UNSUPPORTED_GRANULARITY` | correctable | The requested `time_granularity` on `get_media_buy_delivery` is not in the product's declared `reporting_capabilities.windowed_pull_granularities`. Distinct from `UNSUPPORTED_FEATURE` (generic capability mismatch) by being narrowly about reporting-window granularity — the buyer asked for hourly pull-recovery on a product that only honors daily pulls, for example. Sellers MAY echo the declared set in `error.details.supported_granularities` when the caller is authorized to read the product's reporting capabilities — the same set is already available via `get_adcp_capabilities`, so the echo is a convenience, not load-bearing. Sellers MUST NOT echo a granularity set the caller could not otherwise read (per-product capability views vary by buyer entitlement). The `error.field` SHOULD point at `time_granularity`. Buyers that need higher-frequency recovery than the seller's pull set supports MUST rely on the webhook channel as primary at that frequency — the seller's `available_reporting_frequencies` may legitimately exceed `windowed_pull_granularities` (e.g., a stream-tap webhook on Kafka with warehouse pulls only at daily). |
| `UNSUPPORTED_PROVISIONING` | correctable | The seller does not support the `sync_accounts` mode the entry requested. Returned per-entry in the `sync_accounts` response when (a) an entry keyed by the natural-key trio (`brand` + `operator` + `billing`) is sent to a seller that does not provision accounts via AdCP — typical for account-id namespaces where accounts are pre-provisioned out of band or discovered via `list_accounts`; or (b) an entry keyed by `account` (AccountRef) is sent to a seller that has not implemented the settings-update mode. Distinct from `UNSUPPORTED_FEATURE` (generic capability mismatch) by being narrowly about which of the two `sync_accounts` modes the seller implements. The two modes are mutually exclusive per-entry — the seller MUST NOT silently downgrade or upgrade between them. Sellers MAY declare which modes they implement via `get_adcp_capabilities` (forward-looking — capability declaration shape is open). The `error.field` SHOULD point at the entry index where the unsupported shape was found. |
| `VALIDATION_ERROR` | correctable | Request contains invalid field values or violates business rules beyond schema validation. |
| `VERSION_UNSUPPORTED` | correctable | The declared adcp_version (release-precision) or adcp_major_version (deprecated) is not supported by this seller. The error details SHOULD follow `error-details/version-unsupported.json` — `supported_versions` (release-precision strings) is authoritative for retry; `supported_majors` is deprecated. |

Unknown codes: fall back to the HTTP status code (4xx = correctable, 5xx = transient).

## Test Scenarios

Run compliance tests with `adcp test <agent> <scenario>`. 24 built-in scenarios:

| Scenario | What it tests |
|----------|---------------|
| `health_check` | Basic connectivity check - verify agent responds |
| `discovery` | Test get_products, list_creative_formats, list_authorized_properties |
| `create_media_buy` | Discovery + create a test media buy (sandbox) |
| `full_sales_flow` | Full lifecycle: discovery → create → update → delivery |
| `creative_sync` | Test sync_creatives flow |
| `creative_inline` | Test inline creatives in create_media_buy |
| `creative_flow` | Creative agent: list_formats → build → preview |
| `signals_flow` | Signals agent: get_signals → activate |
| `error_handling` | Verify agent returns proper error responses |
| `validation` | Test schema validation (invalid inputs should be rejected) |
| `pricing_edge_cases` | Test auction vs fixed pricing, min spend, bid_price |
| `temporal_validation` | Test date/time ordering and format validation |
| `behavior_analysis` | Analyze agent behavior: auth, brief relevance, filtering |
| `response_consistency` | Check for schema errors, pagination bugs, data mismatches |
| `capability_discovery` | Test get_adcp_capabilities and verify v3 protocol support |
| `governance_property_lists` | Test property list CRUD (create, get, update, delete) |
| `governance_content_standards` | Test content standards listing and calibration |
| `si_session_lifecycle` | Test full SI session: initiate → messages → terminate |
| `si_availability` | Quick check for SI offering availability |
| `campaign_governance` | Full governance lifecycle: sync_plans → check → execute → report |
| `campaign_governance_denied` | Denied flow: over-budget, unauthorized market |
| `campaign_governance_conditions` | Conditions flow: apply conditions → re-check |
| `campaign_governance_delivery` | Delivery monitoring with drift detection |
| `seller_governance_context` | Verify seller persists governance_context from media buy lifecycle |

**Deep dive:** Storyboard YAML definitions live at `https://adcontextprotocol.org/compliance/{version}/` and are mirrored locally in `compliance/cache/{version}/` after `npm run sync-schemas`.

**Fictional entities:** `compliance/cache/{version}/universal/fictional-entities.yaml` defines all fictional companies used in storyboards and training (advertisers, agencies, publishers, data providers). Aligned to the character bible at docs.adcontextprotocol.org/specs/character-bible. All domains use the `.example` TLD. Sandbox brands (advertisers) are resolvable via AgenticAdvertising.org.

### Seeding fixtures for compliance (seller-side)

Group A storyboards seed fixtures via `comply_test_controller.seed_product` (and the other `seed_*` scenarios) before calling the spec tool. Two SDK helpers bridge this:

- **`mergeSeedProduct`** (plus `mergeSeedPricingOption`, `mergeSeedCreative`, `mergeSeedPlan`, `mergeSeedMediaBuy`): permissive merge of a sparse storyboard fixture onto the seller's baseline defaults. `undefined`/`null` keep base; arrays replace by default; well-known id-keyed lists (`pricing_options`, `publisher_properties`, `packages`, `assets`, plan `findings`) overlay by id so seeding one entry doesn't drop the rest.
- **`bridgeFromTestControllerStore(store, productDefaults)`**: wires a `Map<string, unknown>` seed store into `get_products` responses automatically. Sandbox requests merge seeded + handler products (seeded wins collisions); production traffic (no sandbox marker, or a resolved non-sandbox account) skips the bridge.

Wire on `createAdcpServerFromPlatform(platform, { testController: bridgeFromTestControllerStore(store, baseline) })`. See `skills/build-seller-agent/SKILL.md` for the full pattern alongside `createComplyController`.

### Anti-façade upstream-traffic recording (`@adcp/sdk/upstream-recorder`)

Storyboards declaring `check: upstream_traffic` (runner-output-contract v2.0.0, spec PR adcontextprotocol/adcp#3816) verify that an adapter actually called its upstream platform with the storyboard-supplied identifiers — distinguishing a real adapter from one returning shape-valid AdCP responses without touching upstream. Adopters opt in by advertising `query_upstream_traffic` on their `comply_test_controller`.

`@adcp/sdk/upstream-recorder` is the producer-side reference middleware: a sandbox-only-by-default helper that wraps the adapter's HTTP layer with per-principal isolation, record-time secret redaction, ring-buffer + TTL eviction, and a `query()` method that maps onto the controller wire shape via `toQueryUpstreamTrafficResponse()`. Wire-up is four steps — boot recorder, wrap fetch, scope handlers in `runWithPrincipal`, return `toQueryUpstreamTrafficResponse(recorder.query(...))` from your `comply_test_controller`'s `query_upstream_traffic` scenario. Worked example at `examples/hello_signals_adapter_marketplace.ts`, including multi-tenant principal resolution.

By default the runner requests `attestation_mode: "raw"`, so returned calls include the redacted `payload` plus `payload_length`. For identifier-only checks it requests `attestation_mode: "digest"` with `identifier_value_digests`; the recorder can then omit raw payloads and return `identifier_match_proofs` showing which hashed storyboard values were observed. Storyboards that require payload introspection can declare `attestation_mode_required: "raw"`; if a controller only returns digest attestations, those payload assertions grade `not_applicable` rather than inspecting unavailable bodies.

## Key Types

See docs/TYPE-SUMMARY.md for field-level detail. Key types at a glance:

| Type | Purpose |
|------|---------|
| `AgentConfig` | Agent connection config (uri, protocol, auth) |
| `TaskResult<T>` | Return type of every tool call (status + data/error/adcpError/correlationId/deferred/submitted; metadata includes seller-served `adcpVersion`) |
| `InputHandler` | Callback for agent clarification requests |
| `ConversationContext` | Passed to InputHandler with messages, question, helpers |
| `Product` | Advertising inventory item with formats, pricing, targeting |
| `MediaBuy` | Purchased campaign with packages, budget, schedule |
| `CreativeAsset` | Creative with type, format, dimensions, status |
| `Targeting` | Audience criteria (geo, demo, behavioral, contextual, device) |
| `PricingOption` | Price model (CPM, vCPM, CPC, CPCV, CPV, CPP, CPA, FlatRate, Time) |
| `GovernanceConfig` | Buyer-side governance middleware config |

## Task Statuses

Every tool call returns a `TaskResult` with one of these statuses:

- `completed` — Success. Data in `result.data`.
- `input-required` — Agent needs clarification. Use `InputHandler` or `result.deferred.resume(answer)`.
- `submitted` — Long-running. Poll via `result.submitted.waitForCompletion()` or use webhooks.
- `working` — In progress (intermediate, usually not seen by callers).
- `deferred` — Requires human decision. Token in `result.deferred.token`.
- `governance-denied` — Blocked by governance middleware.

**Deep dive:** docs/guides/ASYNC-DEVELOPER-GUIDE.md, docs/guides/ASYNC-API-REFERENCE.md

## Protocols

AdCP tools are served over MCP (Model Context Protocol) or A2A (Agent-to-Agent). The client auto-detects based on `AgentConfig.protocol`. MCP endpoints end with `/mcp/`. Auth is via bearer token in `x-adcp-auth` header.

**Deep dive:** docs/development/PROTOCOL_DIFFERENCES.md

## Discovery

Publishers declare agents in `/.well-known/adagents.json`. Brands declare identity in `/.well-known/brand.json`. Use `PropertyCrawler` or `adcp registry` CLI to discover agents.

## Where to Read More

These docs are available locally in the repo and hosted at https://adcontextprotocol.github.io/adcp-client/

| Need | Local path | Hosted |
|------|-----------|--------|
| Full type signatures | docs/TYPE-SUMMARY.md | [link](https://adcontextprotocol.github.io/adcp-client/TYPE-SUMMARY.md) |
| Getting started / install | docs/getting-started.md | [link](https://adcontextprotocol.github.io/adcp-client/getting-started.md) |
| Build a server-side agent | docs/guides/BUILD-AN-AGENT.md | [link](https://adcontextprotocol.github.io/adcp-client/guides/BUILD-AN-AGENT.md) |
| Migrating 6.7 → 6.9 (skips deprecated 6.8.0; 13 additive recipes; 2 breaking) | docs/migration-6.7-to-6.9.md | [link](https://adcontextprotocol.github.io/adcp-client/migration-6.7-to-6.9.md) |
| Migrating 6.6 → 6.7 (15 recipes; 2 breaking) | docs/migration-6.6-to-6.7.md | [link](https://adcontextprotocol.github.io/adcp-client/migration-6.6-to-6.7.md) |
| Migrating 5.x → 6.x | docs/migration-5.x-to-6.x.md | [link](https://adcontextprotocol.github.io/adcp-client/migration-5.x-to-6.x.md) |
| BuyerAgentRegistry adopter migration | docs/migration-buyer-agent-registry.md | [link](https://adcontextprotocol.github.io/adcp-client/migration-buyer-agent-registry.md) |
| Account resolution: explicit / implicit / derived | docs/guides/account-resolution.md | [link](https://adcontextprotocol.github.io/adcp-client/guides/account-resolution.md) |
| ctx_metadata credential safety | docs/guides/CTX-METADATA-SAFETY.md | [link](https://adcontextprotocol.github.io/adcp-client/guides/CTX-METADATA-SAFETY.md) |
| Request signing (RFC 9421) + JWKS | docs/guides/SIGNING-GUIDE.md | [link](https://adcontextprotocol.github.io/adcp-client/guides/SIGNING-GUIDE.md) |
| Conformance (property-based fuzzing) | docs/guides/CONFORMANCE.md | [link](https://adcontextprotocol.github.io/adcp-client/guides/CONFORMANCE.md) |
| Validate your agent (5-command checklist) | docs/guides/VALIDATE-YOUR-AGENT.md | [link](https://adcontextprotocol.github.io/adcp-client/guides/VALIDATE-YOUR-AGENT.md) |
| Async patterns (polling, webhooks, deferred) | docs/guides/ASYNC-DEVELOPER-GUIDE.md | [link](https://adcontextprotocol.github.io/adcp-client/guides/ASYNC-DEVELOPER-GUIDE.md) |
| Async API reference | docs/guides/ASYNC-API-REFERENCE.md | [link](https://adcontextprotocol.github.io/adcp-client/guides/ASYNC-API-REFERENCE.md) |
| Input handler patterns | docs/guides/HANDLER-PATTERNS-GUIDE.md | [link](https://adcontextprotocol.github.io/adcp-client/guides/HANDLER-PATTERNS-GUIDE.md) |
| Webhook configuration | docs/guides/PUSH-NOTIFICATION-CONFIG.md | [link](https://adcontextprotocol.github.io/adcp-client/guides/PUSH-NOTIFICATION-CONFIG.md) |
| Real-world code examples | docs/guides/REAL-WORLD-EXAMPLES.md | [link](https://adcontextprotocol.github.io/adcp-client/guides/REAL-WORLD-EXAMPLES.md) |
| CLI reference | docs/CLI.md | [link](https://adcontextprotocol.github.io/adcp-client/CLI.md) |
| Zod runtime validation | docs/ZOD-SCHEMAS.md | [link](https://adcontextprotocol.github.io/adcp-client/ZOD-SCHEMAS.md) |
| Testing strategy | docs/guides/TESTING-STRATEGY.md | [link](https://adcontextprotocol.github.io/adcp-client/guides/TESTING-STRATEGY.md) |
| Testing `composeMethod`-wrapped handlers | docs/recipes/composeMethod-testing.md | [link](https://adcontextprotocol.github.io/adcp-client/recipes/composeMethod-testing.md) |
| Protocol differences (MCP vs A2A) | docs/development/PROTOCOL_DIFFERENCES.md | [link](https://adcontextprotocol.github.io/adcp-client/development/PROTOCOL_DIFFERENCES.md) |
| TypeDoc API reference | docs/api/index.html | [link](https://adcontextprotocol.github.io/adcp-client/api/index.html) |

JSON schemas (source of truth): `schemas/cache/latest/index.json` (local only)

## External Resources

- Documentation: https://adcontextprotocol.github.io/adcp-client/
- npm: https://www.npmjs.com/package/@adcp/sdk
- Spec: https://adcontextprotocol.org
- CLI: `npx @adcp/sdk@adcp-3.1` for the 8.1 / AdCP 3.1 beta line
