> ## Documentation Index
> Fetch the complete documentation index at: https://docs.adcontextprotocol.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Calling an AdCP agent

> Wire-level invariants every AdCP buyer must follow: idempotency_key replay, account oneOf variants, async status:'submitted' polling, and error recovery from adcp_error.issues[].

# Calling an AdCP agent

This page is the canonical buyer-side wire contract: the rules that don't live cleanly in any single task schema, but apply to every mutating call you'll make. If you're building a buyer (DSP, planning tool, agentic client) and calling out to AdCP sales, creative, signals, governance, SI, or brand agents, read this once.

The agent-facing version of this content lives at [`skills/call-adcp-agent/SKILL.md`](https://github.com/adcontextprotocol/adcp/blob/main/skills/call-adcp-agent/SKILL.md) — bundled into the [protocol tarball](/dist/docs/3.2.0-beta.0/building/by-layer/L0/schemas#one-shot-protocol-bundle) so SDKs can ship it to coding agents.

## Discovery chain

Walk these in order on first contact with any new agent:

1. **Agent card** (A2A) or **`tools/list`** (MCP): returns the tools selected for this endpoint or session. An AdCP 3.2 MCP server includes each selected tool's self-contained `inputSchema`; it does not load the complete AdCP catalog or its response schemas into model context. Treat the live names as the authority for what can be called.
2. **[`get_adcp_capabilities`](/dist/docs/3.2.0-beta.0/protocol/get_adcp_capabilities)**: returns supported protocols, AdCP major versions, and feature flags. Tells you *which* tools this agent supports, not how to call them. See [`get_adcp_capabilities`](/dist/docs/3.2.0-beta.0/protocol/get_adcp_capabilities).
3. **`get_schema(tool_name)`** *(when the agent exposes it — pending standardization, see [#3057](https://github.com/adcontextprotocol/adcp/issues/3057))*: returns the JSON Schema for a specific tool's request/response.
4. **Bundled schemas** (offline, authoritative): every published AdCP version ships JSON Schemas for every tool, signed via Sigstore. The path differs by SDK — the spec repo source uses `dist/schemas/<version>/bundled/`, `@adcp/sdk` puts them at `schemas/cache/<version>/bundled/` after `npm run sync-schemas`, Python and Go SDKs use their own conventions. Don't hardcode a path; let the SDK's loader find them. Once located, each schema lives at `<protocol>/<tool>-{request,response}.json`.

## Idempotency: replay vs. new operation

Every mutating tool requires an `idempotency_key` (UUID).

* **Same key on retry** → server replays the **same response**, byte-for-byte. Use this for transport-level retries (timeout, 5xx, dropped connection).
* **Fresh key** → **new operation**, regardless of body. Generating a new UUID because the previous attempt failed is the most common way naïve callers create duplicate media buys.
* **Same key, different canonical body** → [`IDEMPOTENCY_CONFLICT`](/dist/docs/3.2.0-beta.0/building/verification/compliance-catalog#error-code-idempotency-conflict). Sellers MUST reject (rule 5 in [security.mdx#idempotency](/dist/docs/3.2.0-beta.0/building/by-layer/L1/security#idempotency)) — do not silently apply the second body, do not silently replay the first response.
* **Same key while first request still running** → [`IDEMPOTENCY_IN_FLIGHT`](/dist/docs/3.2.0-beta.0/building/verification/compliance-catalog#error-code-idempotency-in-flight) (rule 9 in [security.mdx#idempotency](/dist/docs/3.2.0-beta.0/building/by-layer/L1/security#idempotency)). The seller MAY return this code with top-level `error.retry_after` instead of blocking. Wait and retry with the **same key** — minting a fresh key on this code turns a safe retry into a double-execution race.

For async flows, the replayed response carries the **same `task_id`** so polling continues against the same task instead of forking.

The compact 3.2 lifecycle requires `idempotency_key` on [`request_proposals`](/dist/docs/3.2.0-beta.0/media-buy/task-reference/request_proposals), [`refine_proposals`](/dist/docs/3.2.0-beta.0/media-buy/task-reference/refine_proposals), [`decline_proposals`](/dist/docs/3.2.0-beta.0/media-buy/task-reference/decline_proposals), [`buy_products`](/dist/docs/3.2.0-beta.0/media-buy/task-reference/buy_products), [`accept_proposal`](/dist/docs/3.2.0-beta.0/media-buy/task-reference/accept_proposal), and [`control_media_buy`](/dist/docs/3.2.0-beta.0/media-buy/task-reference/control_media_buy). [`list_products`](/dist/docs/3.2.0-beta.0/media-buy/task-reference/list_products) remains a synchronous key-optional read.

`idempotency_key` is required on: [`create_media_buy`](/dist/docs/3.2.0-beta.0/media-buy/task-reference/create_media_buy), [`update_media_buy`](/dist/docs/3.2.0-beta.0/media-buy/task-reference/update_media_buy), [`sync_creatives`](/dist/docs/3.2.0-beta.0/creative/task-reference/sync_creatives), [`sync_audiences`](/dist/docs/3.2.0-beta.0/media-buy/task-reference/sync_audiences), [`sync_accounts`](/dist/docs/3.2.0-beta.0/accounts/tasks/sync_accounts), [`sync_catalogs`](/dist/docs/3.2.0-beta.0/media-buy/task-reference/sync_catalogs), [`sync_event_sources`](/dist/docs/3.2.0-beta.0/media-buy/task-reference/sync_event_sources), [`sync_plans`](/dist/docs/3.2.0-beta.0/governance/campaign/tasks/sync_plans), [`sync_governance`](/dist/docs/3.2.0-beta.0/accounts/tasks/sync_governance), [`activate_signal`](/dist/docs/3.2.0-beta.0/signals/tasks/activate_signal), [`acquire_rights`](/dist/docs/3.2.0-beta.0/brand-protocol/tasks/acquire_rights), [`log_event`](/dist/docs/3.2.0-beta.0/media-buy/task-reference/log_event), [`report_usage`](/dist/docs/3.2.0-beta.0/accounts/tasks/report_usage), [`provide_performance_feedback`](/dist/docs/3.2.0-beta.0/media-buy/task-reference/provide_performance_feedback), [`report_plan_outcome`](/dist/docs/3.2.0-beta.0/governance/campaign/tasks/report_plan_outcome), [`create_property_list`](/dist/docs/3.2.0-beta.0/governance/property/tasks/property_lists#create_property_list), [`update_property_list`](/dist/docs/3.2.0-beta.0/governance/property/tasks/property_lists#update_property_list), [`delete_property_list`](/dist/docs/3.2.0-beta.0/governance/property/tasks/property_lists#delete_property_list), [`create_collection_list`](/dist/docs/3.2.0-beta.0/governance/collection/tasks/collection_lists#create_collection_list), [`update_collection_list`](/dist/docs/3.2.0-beta.0/governance/collection/tasks/collection_lists#update_collection_list), [`delete_collection_list`](/dist/docs/3.2.0-beta.0/governance/collection/tasks/collection_lists#delete_collection_list), [`create_content_standards`](/dist/docs/3.2.0-beta.0/governance/content-standards/tasks/create_content_standards), [`update_content_standards`](/dist/docs/3.2.0-beta.0/governance/content-standards/tasks/update_content_standards), [`calibrate_content`](/dist/docs/3.2.0-beta.0/governance/content-standards/tasks/calibrate_content), [`si_initiate_session`](/dist/docs/3.2.0-beta.0/sponsored-intelligence/tasks/si_initiate_session), [`si_send_message`](/dist/docs/3.2.0-beta.0/sponsored-intelligence/tasks/si_send_message).

Missing the key → `adcp_error.code: 'VALIDATION_ERROR'` with `/idempotency_key` in `issues`.

## `account` is `oneOf` — pick exactly one variant

`account` is a discriminated union. On [`create_media_buy`](/dist/docs/3.2.0-beta.0/media-buy/task-reference/create_media_buy) and [`update_media_buy`](/dist/docs/3.2.0-beta.0/media-buy/task-reference/update_media_buy), two variants:

```json theme={null}
// variant 0: by seller-assigned id (from list_accounts or out-of-band onboarding;
// buyer-declared sellers may also echo account_id from sync_accounts as an internal handle)
"account": { "account_id": "seller_assigned_id" }

// variant 1: by advertiser natural key
// (brand + operator, optional operator_unit/currency/timezone/sandbox;
// brand may include commercial-identity countries[])
//   brand.domain — the buyer's brand domain (e.g., advertiser website)
//   operator     — the buyer-side entity operating on behalf of the brand
"account": { "brand": { "domain": "acme.com" }, "operator": "pinnacle-media.com" }
```

**Do NOT merge required fields across variants.** `additionalProperties: false` on each variant means `{account_id, brand}` fails BOTH.

When a task schema requires `account`, send an explicit `AccountRef` even if the SDK auto-selected the only account available to the authenticated credential. Hidden credential-implied defaulting is not a protocol model. When a task marks `account` optional, omission has only the semantics documented by that task.

Other tools (e.g. [`sync_creatives`](/dist/docs/3.2.0-beta.0/creative/task-reference/sync_creatives)) may accept a superset — always check the specific tool's schema.

## Async responses: `status: 'submitted'` means queued

A mutating tool can return one of three shapes:

```json theme={null}
// Success (sync): the work is done
{ "media_buy_id": "mb_123", "packages": [...], "confirmed_at": "..." }

// Submitted (async): the work is queued
{ "status": "submitted", "task_id": "tk_abc", "message": "Awaiting IO signature" }

// Error: don't retry without fixing
{ "errors": [{ "code": "PRODUCT_NOT_FOUND", "message": "..." }] }
```

AdCP task state is an **application-layer** contract. MCP and A2A may wrap, stream, or transport an AdCP response, but their native task mechanisms do not replace the AdCP `task_id`, status values, webhook payloads, or polling/reconciliation surfaces. A transport task can complete after delivering an AdCP response whose payload still says `status: 'submitted'`.

When you see `status: 'submitted'`, the work is **not** complete. Under the [AdCP A2A Profile Extension v3](/dist/docs/3.2.0-beta.0/building/by-layer/L0/a2a-profile-extension), the caller MUST poll by sending fresh structured invocations of [`get_task_status`](https://adcontextprotocol.org/schemas/3.2.0-beta.0/protocol/get-task-status-request.json) with the returned AdCP `task_id`. The enclosing A2A Task is already completed and MUST NOT be polled as a proxy for the AdCP operation. MCP implementations may continue to expose the legacy AdCP `tasks/get` surface; both AdCP polling names use the same snake\_case payload shape, including the optional `account` scope for multi-account credentials.

Pass `include_result: true` when polling so the seller includes the completion payload once status transitions to `completed`:

```json theme={null}
// get_task_status request
{
  "task_id": "task_456",
  "include_result": true,
  "account": {
    "brand": { "domain": "acmeoutdoor.example" },
    "operator": "pinnacle-agency.example",
    "sandbox": true
  }
}

// get_task_status response — completed
{
  "task_id": "task_456",
  "task_type": "create_media_buy",
  "protocol": "media-buy",
  "status": "completed",
  "completed_at": "2025-01-22T10:30:00Z",
  "result": {
    "media_buy_id": "mb_12345",
    "packages": [{ "package_id": "pkg_001" }]
  }
}
```

The `result` field uses the same payload structure as the push-notification webhook `result` field for completed tasks — buyers who configure both polling and webhooks receive the same data shape either way.

## Error recovery — read `issues[]`

Every validation failure produces an envelope shaped like:

```json theme={null}
{
  "adcp_error": {
    "code": "VALIDATION_ERROR",
    "recovery": "correctable",
    "field": "/first/offending/pointer",
    "issues": [
      {
        "pointer": "/account",
        "keyword": "oneOf",
        "message": "must match exactly one schema in oneOf",
        "variants": [
          { "index": 0, "required": ["account_id"],        "properties": ["account_id"] },
          { "index": 1, "required": ["brand", "operator"], "properties": ["brand", "operator", "sandbox"] }
        ]
      },
      { "pointer": "/brand/domain", "keyword": "required", "message": "must have required property 'domain'" }
    ]
  }
}
```

* `issues[].pointer` — RFC 6901 JSON Pointer to the offending field
* `issues[].keyword` — Ajv keyword (`required`, `type`, `oneOf`, `anyOf`, `additionalProperties`, `format`, `enum`)
* `issues[].variants` — when `keyword` is `oneOf` or `anyOf`, each entry lists one variant's `required` + declared `properties`

**For `oneOf` failures, pick ONE variant from `variants[]` and send only its `required` fields.** This is the fastest recovery path when you didn't know the field was a union.

`recovery` values:

* `correctable` — buyer-side fix; read `issues[]`, patch the pointers, resend
* `transient` — retry with the **same** `idempotency_key`
* `terminal` — requires human action (account suspended, payment required); do not retry

## Common shape pitfalls

| Symptom                                                  | What it means                                                         | Fix                                                                                                                                                                                             |
| -------------------------------------------------------- | --------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `keyword: 'oneOf'` with `variants[]`                     | Discriminated union — you sent fields from multiple variants, or none | Pick ONE variant from `variants[]`. Send only its `required` fields.                                                                                                                            |
| 2-3 `additionalProperties` errors at the same pointer    | You merged `oneOf` variants                                           | Drop to one variant. Don't keep "extra" fields "for completeness".                                                                                                                              |
| `keyword: 'required'`, `pointer: '/idempotency_key'`     | Mutating tool, no UUID                                                | Generate fresh UUID per logical operation. Reuse on retries.                                                                                                                                    |
| `keyword: 'type'` or `additionalProperties` at `/budget` | Sent `{amount, currency}`                                             | `budget` is a number. Currency is implied by `pricing_option_id`.                                                                                                                               |
| `enum` at `/format_kind`                                 | Sent a non-canonical format kind                                      | Choose a registered canonical kind or use a valid `custom` declaration.                                                                                                                         |
| `keyword: 'enum'` at `/destinations/*/type`              | Made-up destination type                                              | Use `'platform'` (with `platform`) or `'agent'` (with `agent_url`).                                                                                                                             |
| Response carries `status: 'submitted'` and `task_id`     | Async — work is queued, NOT done                                      | On the AdCP v3 A2A profile, invoke [`get_task_status`](https://adcontextprotocol.org/schemas/3.2.0-beta.0/protocol/get-task-status-request.json). On MCP, use the advertised AdCP polling task. |

## Transport notes

* **MCP**: `tools/call` with `{ name: 'tool_name', arguments: {...} }`. Read `structuredContent` for the typed response.
* **A2A 1.0**: advertise and activate `https://adcontextprotocol.org/extensions/adcp/v3`, then Send Message with exactly one invocation DataPart shaped `{ skill: 'tool_name', input: {...} }`; optional TextParts are advisory. Read the authoritative response DataPart from the completed Task artifact.

Both transports share idempotency, error shape, schema enforcement, and handler semantics. If a call works on one, the equivalent call works on the other.

A common trap: **A2A `Task.status.state: 'completed'` is not the same as AdCP completion.** A2A task state describes the transport invocation; AdCP-level completion is in the artifact DataPart. A `completed` A2A task can carry a `submitted` AdCP response, whose `task_id` remains only in that DataPart and is polled through [`get_task_status`](https://adcontextprotocol.org/schemas/3.2.0-beta.0/protocol/get-task-status-request.json). It also carries structured business outcomes that are not transport failures, such as `GetProductsRejected`: A2A remains `completed`, while the artifact payload contains `status: "rejected"`, `reason`, and optional `suggestions[]`.

## Related

* Per-task request/response shapes: see the protocol-specific reference (`/docs/media-buy/`, `/docs/creative/`, `/docs/signals/`, etc.).
* [Protocol architecture](/dist/docs/3.2.0-beta.0/protocol/architecture) — how the protocol domains fit together.
* [Required tasks](/dist/docs/3.2.0-beta.0/protocol/required-tasks) — which tasks an agent must implement to claim a specialism.
* [`get_adcp_capabilities`](/dist/docs/3.2.0-beta.0/protocol/get_adcp_capabilities) — first call against any new agent.
* [Schemas](/dist/docs/3.2.0-beta.0/building/by-layer/L0/schemas) — how SDKs consume the protocol tarball (which now bundles `skills/`).
* [Build a caller](/dist/docs/3.2.0-beta.0/building/by-layer/L4/build-a-caller) — build-shaped guide for the caller side: install, call, handle responses, ingest reporting.
