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

# Build a caller

> Client-side guide for AdCP buyers and demand-side applications. Install the SDK, discover an agent's capabilities, make calls, handle async responses and errors, and ingest reporting. Weeks of handler glue, not months.

If you're building the **buy side** — a DSP, planning tool, agentic client, or any application that calls AdCP agents to plan, buy, and report — start here. Caller-side L0–L3 is weeks of handler glue, not the [3–4 person-month build](/docs/building/cross-cutting/sdk-stack#why-sdks-matter-more-in-adcp-than-in-eg-http) the agent side requires. The full-stack SDK in your language carries L0–L3; you write the calling logic, response handling, and whatever your application does with the data.

<Note>
  **Spec-level reference vs build-shaped guide.** This page walks you through the build. The wire-level invariants — every rule that applies to every mutating call — live at [Calling an AdCP agent](/docs/protocol/calling-an-agent). Read it once before going to production; refer back whenever you're debugging a wire-shape error.
</Note>

<Tip>
  **Try it against a live agent.** AAO runs a public test agent at `https://test-agent.adcontextprotocol.org` with per-domain endpoints — `/sales/mcp`, `/creative/mcp`, `/signals/mcp`, `/governance/mcp`. Point your client at the matching endpoint and `getAdcpCapabilities()` works without auth. Use it to verify your install before pointing at a real seller.
</Tip>

## Install the SDK

The same SDKs that ship server primitives also ship the calling client. Install one and you have both.

<Tabs>
  <Tab title="JavaScript/TypeScript">
    ```bash theme={null}
    npm install @adcp/sdk
    ```

    ```typescript theme={null}
    import { createSingleAgentClient } from '@adcp/sdk';

    const client = createSingleAgentClient({
      id: 'sales',
      name: 'Sales agent',
      agent_uri: 'https://sales.example.com/mcp',
      protocol: 'mcp',
    });
    ```

    For multi-agent fan-out (one client driving many sellers in parallel) use `ADCPMultiAgentClient` instead — same call surface, indexed by agent id.

    * [NPM Package](https://www.npmjs.com/package/@adcp/sdk)
    * [GitHub Repository](https://github.com/adcontextprotocol/adcp-client)
  </Tab>

  <Tab title="Python">
    ```bash theme={null}
    pip install adcp
    ```

    ```python theme={null}
    from adcp import ADCPClient, AgentConfig, Protocol

    client = ADCPClient(AgentConfig(
        id="sales",
        agent_uri="https://sales.example.com/mcp",
        protocol=Protocol.MCP,
    ))
    ```

    * [PyPI Package](https://pypi.org/project/adcp/)
    * [GitHub Repository](https://github.com/adcontextprotocol/adcp-client-python)
  </Tab>

  <Tab title="Go">
    ```bash theme={null}
    go get github.com/adcontextprotocol/adcp-go/adcp
    ```

    The Go caller surface is in active development. See the [adcp-go README](https://github.com/adcontextprotocol/adcp-go) for current coverage.
  </Tab>
</Tabs>

## Authenticate

Most agents require credentials before they'll respond to anything beyond `get_adcp_capabilities`. The SDK accepts auth at construction:

<Tabs>
  <Tab title="Bearer / API key">
    ```typescript theme={null}
    const client = createSingleAgentClient({
      id: 'sales',
      name: 'Sales agent',
      agent_uri: 'https://sales.example.com/mcp',
      protocol: 'mcp',
      auth_token: process.env.ADCP_API_KEY,
    });
    ```
  </Tab>

  <Tab title="Signed requests (RFC 9421)">
    ```typescript theme={null}
    const client = createSingleAgentClient({
      id: 'sales',
      name: 'Sales agent',
      agent_uri: 'https://sales.example.com/mcp',
      protocol: 'mcp',
      signing: { keyId: 'your-key-id', privateKey: /* PEM or KMS handle */ },
    });
    ```
  </Tab>
</Tabs>

If your first call returns a 401 / `AUTH_REQUIRED`, the credentials aren't reaching the agent — check the constructor option, not the request payload. See the [L1 security implementation profile](/docs/building/by-layer/L1/security) for the full credential model.

## First call: discover the agent

Before calling tools by hand, ask the agent what it supports.

```typescript theme={null}
const capabilities = await client.getAdcpCapabilities();
// → { supported_protocols: [...], adcp_versions: [...], features: {...} }
```

`get_adcp_capabilities` returns the agent's protocol coverage, AdCP version range, and feature flags. Use it to gate calls — if `media_buy` isn't in `supported_protocols`, don't call `create_media_buy` against this agent. See the [`get_adcp_capabilities`](/docs/protocol/get_adcp_capabilities) reference for the full response shape.

For the rest of the discovery chain (agent card, `tools/list`, `get_schema`), see the [Discovery chain section of Calling an agent](/docs/protocol/calling-an-agent#discovery-chain).

## Make a call

Typed methods on the client correspond to AdCP tools. The SDK validates your request against the bundled schemas before sending and parses the response into a typed value.

```typescript theme={null}
const products = await client.getProducts({
  brief: 'Video campaign for pet owners, 18–34, US, $50K monthly',
});

const buy = await client.createMediaBuy({
  idempotency_key: crypto.randomUUID(),
  account: { brand: { domain: 'acme.com' }, operator: 'sales.example' },
  packages: [/* … */],
});
```

Two things worth knowing on the first call:

* **Generate a fresh `idempotency_key` per logical operation.** Same key on retry → server replays the same response. Fresh key after a failure creates duplicate buys. See the [Idempotency rules](/docs/protocol/calling-an-agent#idempotency-replay-vs-new-operation).
* **`account` is a discriminated `oneOf`.** Pick one variant (`{account_id}` from `sync_accounts` / `list_accounts`, or `{brand, operator}` as the natural key — `brand.domain` is the buyer's brand domain, `operator` is the seller agent's deployment hostname or brand.json identifier) and send only its required fields. Merging them fails both. See [`account` is `oneOf`](/docs/protocol/calling-an-agent#account-is-oneof--pick-exactly-one-variant).

## Handle the three response shapes

Every mutating tool returns one of three shapes. Handle them explicitly.

```typescript theme={null}
const response = await client.createMediaBuy({/* … */});

if ('errors' in response) {
  // Error: don't retry without fixing — read response.adcp_error.issues[]
  // for correctable failures (validation, oneOf, etc.)
} else if (response.status === 'submitted') {
  // Async: the work is queued, NOT done. The completion payload arrives
  // later — either via webhook (preferred) or by polling the AdCP task.
} else {
  // Sync success: response carries the completion payload directly.
  // (e.g., response.media_buy_id, response.packages)
}
```

The SDK steers you to webhooks for async completion. Configure `webhookUrlTemplate` and a status-change handler at construction; the SDK verifies the inbound webhook against the seller's JWKS and fires your handler with the same `result` shape that synchronous responses carry — see [Receive webhooks](#receive-webhooks) below.

If you must poll instead of using webhooks (e.g. inside a one-shot script), call the AdCP polling surface: `get_task_status` when the seller advertises it, otherwise legacy AdCP `tasks/get` in 3.x. The [Async responses section of Calling an agent](/docs/protocol/calling-an-agent#async-responses-status-submitted-means-queued) has the wire contract.

## Recover from errors

When you see `adcp_error` in a response, read `issues[]` and act based on `recovery`:

```typescript theme={null}
const { code, recovery, issues } = response.adcp_error;

switch (recovery) {
  case 'correctable':
    // Buyer-side fix. Patch the JSON pointers from issues[], resend with
    // the SAME idempotency_key (fresh key = new operation).
    break;
  case 'transient':
    // Retry with the SAME idempotency_key. Same key on retry replays the
    // cached response if the work landed.
    break;
  case 'terminal':
    // Human action required. Don't retry.
    break;
}
```

`issues[]` is the actionable part: each entry has a JSON Pointer (`pointer`), an Ajv keyword (`required`, `oneOf`, `enum`, etc.), and — for `oneOf` failures — a `variants[]` array listing each variant's required fields. See the [Error recovery section](/docs/protocol/calling-an-agent#error-recovery--read-issues) for the full envelope and recovery semantics.

## Receive webhooks

For async tasks, you can either poll or register a webhook. The webhook delivers the same `result` payload as AdCP task polling with `include_result: true`. The SDK ships an RFC 9421 webhook verifier (`createWebhookVerifier` from `@adcp/sdk/signing/server`) that resolves keys via the seller's brand.json, enforces the replay window, and surfaces structured errors for the [webhook error taxonomy](/docs/building/by-layer/L1/security#webhook-error-taxonomy).

If you wire the multi-agent client with a `webhookUrlTemplate` and status-change handlers (per the [@adcp/sdk README](https://github.com/adcontextprotocol/adcp-client#readme)), incoming webhooks are verified and dispatched into your handlers automatically — your HTTP route is one line that hands the request to the client.

For the wire-level requirements your endpoint MUST satisfy regardless of how it's wired (covered components, `content-digest` enforcement, dedup discipline), see [L3 — Webhooks](/docs/building/by-layer/L3/webhooks#signature-verification).

## Ingest reporting

Reporting is read-only and follows the same call/response shape as everything else. Pull delivery for the buys you own and iterate on a windowed cadence:

```typescript theme={null}
const delivery = await client.getMediaBuyDelivery({
  media_buy_ids: ['mb_123', 'mb_456'],
  window: { start: '2026-05-01T00:00:00Z', end: '2026-05-02T00:00:00Z' },
});
```

For sellers that support performance webhooks, you'll receive deltas via the same webhook receiver shown above; otherwise poll on whatever cadence your reporting needs. Caller L4 — optimization, pacing alerts, attribution joins, dashboards — is your application code on top of the typed `delivery` object.

## What you didn't have to write

Caller-side L0–L3 is weeks of handler glue because the SDK already shipped:

* **L0** — typed request builders, response parsers, schema validation against the bundled schemas.
* **L1** — outbound RFC 9421 signing (per-call), inbound webhook verification, key rotation.
* **L2** — agent registry lookup, agent-card publication, credential composition.
* **L3** — async-task polling, webhook receiver, idempotency-key generation helpers, error-recovery classification.

The [SDK stack reference](/docs/building/cross-cutting/sdk-stack) decomposes each layer; the [server-vs-client comparison table](/docs/building/cross-cutting/sdk-stack#server-vs-client-at-each-layer) is the side-by-side view of the cost asymmetry.

## What's next

* **[Calling an agent](/docs/protocol/calling-an-agent)** — the canonical wire-contract reference. Read once before going to production.
* **[Schemas](/docs/building/by-layer/L0/schemas)** — schema bundle, type generation, version pinning.
* **[Webhooks](/docs/building/by-layer/L3/webhooks)** — push notifications, signing, retry, reliability patterns.
* **[Error handling](/docs/building/by-layer/L3/error-handling)** — error categories, codes, recovery classification.
* **[Validate your agent](/docs/building/verification/validate-your-agent)** — storyboards also exist for caller-side wire conformance; run them once your calls are working.

Per-protocol task references:

* [Media buy task reference](/docs/media-buy/task-reference/index)
* [Creative task reference](/docs/creative/task-reference)
* [Signals task reference](/docs/signals/tasks/get_signals)
* [Brand protocol tasks](/docs/brand-protocol)
