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

# 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](/docs/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 tool *names*. AdCP MCP servers no longer publish per-tool parameter schemas in `tools/list` — every tool shows `{type: 'object', properties: {}}`. Don't try to infer shape from there.
2. **`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`](/docs/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`. Sellers MUST reject (rule 5 in [security.mdx#idempotency](/docs/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` (rule 9 in [security.mdx#idempotency](/docs/building/by-layer/L1/security#idempotency)). The seller MAY return this code with `error.details.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.

`idempotency_key` is required on: `create_media_buy`, `update_media_buy`, `sync_creatives`, `sync_audiences`, `sync_accounts`, `sync_catalogs`, `sync_event_sources`, `sync_plans`, `sync_governance`, `activate_signal`, `acquire_rights`, `log_event`, `report_usage`, `provide_performance_feedback`, `report_plan_outcome`, `create_property_list`, `update_property_list`, `delete_property_list`, `create_collection_list`, `update_collection_list`, `delete_collection_list`, `create_content_standards`, `update_content_standards`, `calibrate_content`, `si_initiate_session`, `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` and `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 natural key (brand + operator, optional sandbox)
//   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`) 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. In 3.x, poll via the legacy AdCP `tasks/get` surface using the returned `task_id`. Sellers MAY also advertise the non-colliding `get_task_status` alias; callers MAY use that alias when it appears in discovery. Both AdCP polling names use the same snake\_case payload shape, including the optional `account` scope for multi-account credentials. Do not confuse either AdCP polling shape with transport-native MCP/A2A `tasks/get`, which uses the transport's own task wire shape.

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

```json theme={null}
// tasks/get request (same payload as optional get_task_status alias)
{
  "task_id": "task_456",
  "include_result": true,
  "account": {
    "brand": { "domain": "acmeoutdoor.example" },
    "operator": "pinnacle-agency.example",
    "sandbox": true
  }
}

// tasks/get 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`.                        |
| `additionalProperties` at `/format_id` (string passed)   | Sent `"format_id": "video_..."`                                       | `format_id` is `{agent_url, id}` — always an object.                                     |
| `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                                      | Poll with legacy `tasks/get`, or `get_task_status` when the seller advertises the alias. |

## Transport notes

* **MCP**: `tools/call` with `{ name: 'tool_name', arguments: {...} }`. Read `structuredContent` for the typed response.
* **A2A**: `message/send` with a `DataPart` of shape `{ skill: 'tool_name', input: {...} }`. The typed response is at `task.artifacts[0].parts[0].data`.

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.state: 'completed'` is not the same as AdCP completion.** A2A task state describes the transport call lifecycle; AdCP-level completion is in the artifact's payload (`structuredContent.status` or `data.status`). A `completed` A2A task can still carry a `submitted` AdCP response.

## Related

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