> ## 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.

# A2A Guide

> AdCP A2A integration guide: client setup, agent card verification, SSE streaming for async tasks, artifact handling, and response format for Agent-to-Agent Protocol.

Transport-specific guide for integrating AdCP using the Agent-to-Agent Protocol. For task handling, status management, and workflow patterns, see [Task Lifecycle](/docs/building/by-layer/L3/task-lifecycle).

## A2A Protocol Versions

AdCP tracks the [A2A specification](https://a2a-protocol.org/latest/) under Linux Foundation governance. The **1.0** wire format is the target; **v0.3** remains widely deployed and is supported through the compatibility period.

### What Changed in 1.0

| Area                 | v0.3                               | 1.0                                                                                          |
| -------------------- | ---------------------------------- | -------------------------------------------------------------------------------------------- |
| Agent Card transport | `url` + `protocolVersion` at root  | `supportedInterfaces[]` array with per-interface `url`, `protocolBinding`, `protocolVersion` |
| `Part` discriminator | `kind: "text" \| "data" \| "file"` | No `kind` — content determined by which field is set (`text`, `data`, `url`, `raw`)          |
| File fields          | `uri`, `name`, `mimeType`          | `url` (by reference) or `raw` (base64 bytes), `filename`, `mediaType`                        |
| Message role         | `"user"` / `"agent"`               | `"ROLE_USER"` / `"ROLE_AGENT"` (ProtoJSON canonical)                                         |
| Task state           | `"completed"`, `"working"`, …      | `"TASK_STATE_COMPLETED"`, `"TASK_STATE_WORKING"`, …                                          |
| Timestamps           | ISO-8601                           | ISO-8601 UTC with ms precision (`YYYY-MM-DDTHH:mm:ss.sssZ`)                                  |

AdCP's own unified top-level `status` field (returned by `@adcp/sdk`) continues to use the lowercase shorthand (`"completed"`, `"working"`, …) — that is an AdCP abstraction over the raw A2A `status.state`, not an A2A wire value.

### Dual-Version Compatibility

Servers that need to serve both v0.3 and 1.0 clients advertise both interfaces in their Agent Card and enable explicit compatibility at the transport layer (e.g. `enable_v0_3_compat=True` in the Python SDK). Backward compatibility is **not** enabled by default.

Clients that speak 1.0 can talk to a v0.3 server when the SDK provides downward translation; the reverse (v0.3 client → 1.0-only server) requires the server to enable compat.

### Examples in This Guide

Examples below use **1.0 wire format** (no `kind` field, ProtoJSON enums). For a v0.3 server, the same Part becomes `{ kind: "text", text: "…" }` and states become lowercase. AdCP extraction clients (see [A2A Response Extraction](/docs/building/by-layer/L0/a2a-response-extraction)) accept both shapes during the compatibility period.

## A2A Client Setup

### 1. Initialize A2A Client

```javascript theme={null}
const a2a = new A2AClient({
  endpoint: 'https://adcp.example.com/a2a',
  auth: {
    type: 'bearer',
    token: process.env.ADCP_API_KEY
  },
  agent: {
    name: "AdCP Media Buyer",
    version: "1.0.0"
  }
});
```

### 2. Verify Agent Card

```javascript theme={null}
// Check available skills
const agentCard = await a2a.getAgentCard();
console.log(agentCard.skills.map(s => s.name));
// ["get_products", "create_media_buy", "sync_creatives", ...]
```

### 3. Send Your First Task

```javascript theme={null}
const response = await a2a.send({
  message: {
    role: "ROLE_USER",
    parts: [{
      text: "Find video products for pet food campaign"
    }]
  }
});

// All responses include unified status field (AdCP 1.6.0+)
console.log(response.status);   // "completed" | "input-required" | "working" | etc.
console.log(response.message);  // Human-readable summary
```

## Message Structure (A2A-Specific)

### Multi-Part Messages

A2A's key advantage is multi-part messages combining text, data, and files:

```javascript theme={null}
// Text + structured data + file
const response = await a2a.send({
  message: {
    role: "ROLE_USER",
    parts: [
      {
        text: "Create campaign with these assets"
      },
      {
        data: {
          skill: "create_media_buy",
          parameters: {
            packages: ["pkg_001"],
            total_budget: 100000
          }
        }
      },
      {
        url: "https://cdn.example.com/hero-video.mp4",
        filename: "hero_video_30s.mp4",
        mediaType: "video/mp4"
      }
    ]
  }
});
```

### Skill Invocation Methods

#### Natural Language (Flexible)

```javascript theme={null}
// Agent interprets intent
const task = await a2a.send({
  message: {
    role: "ROLE_USER",
    parts: [{
      text: "Find premium CTV inventory under $50 CPM"
    }]
  }
});
```

#### Explicit Skill (Deterministic)

```javascript theme={null}
// Explicit skill with exact parameters
const task = await a2a.send({
  message: {
    role: "ROLE_USER",
    parts: [{
      data: {
        skill: "get_products",
        parameters: {
          max_cpm: 50,
          channels: ["ctv"],
          tier: "premium"
        }
      }
    }]
  }
});
```

#### Hybrid Approach (Recommended)

```javascript theme={null}
// Context + explicit execution for best results
const task = await a2a.send({
  message: {
    role: "ROLE_USER",
    parts: [
      {
        text: "Looking for inventory for spring campaign targeting millennials"
      },
      {
        data: {
          skill: "get_products",
          parameters: {
            audience: "millennials",
            season: "Q2_2024",
            max_cpm: 45
          }
        }
      }
    ]
  }
});
```

**Status Handling**: See [Task Lifecycle](/docs/building/by-layer/L3/task-lifecycle) for complete status handling patterns.

## A2A Response Format

**New in AdCP 1.6.0**: All responses include unified status field.

### Canonical Response Structure

AdCP responses over A2A **MUST** include at least one DataPart (a Part carrying a `data` field) containing the task response. A TextPart (a Part carrying a `text` field) for human-readable messages is **recommended** but optional.

```json theme={null}
{
  "status": "completed",        // AdCP unified status (see Core Concepts)
  "taskId": "task-123",         // A2A task identifier
  "contextId": "ctx-456",       // Automatic context management
  "artifacts": [{               // A2A-specific artifact structure
    "artifactId": "artifact-product-catalog-abc",
    "name": "product_catalog",
    "parts": [
      {
        "text": "Found 12 video products perfect for pet food campaigns"
      },
      {
        "data": {
          "products": [...],
          "total": 12
        }
      }
    ]
  }]
}
```

The A2A 1.0 wire format carries no `kind` discriminator — the Part's content type is implied by which field is set (`text`, `data`, `url`, or `raw`). For v0.3 servers/clients, the equivalent Part includes `"kind": "text"` / `"kind": "data"` / `"kind": "file"`.

**For complete canonical format specification, see [A2A Response Format](/docs/building/by-layer/L0/a2a-response-format).**

### A2A-Specific Fields

* **taskId**: A2A task identifier for streaming updates
* **contextId**: Automatically managed by A2A protocol
* **artifacts**: Multi-part deliverables with text and data parts
* **status**: AdCP's unified lowercase shorthand, mapped from A2A's `status.state` (see [A2A Response Extraction](/docs/building/by-layer/L0/a2a-response-extraction#wire-format-compatibility))

### Processing Artifacts

AdCP responses use the **last `DataPart` as authoritative** when multiple data parts exist (e.g., from streaming operations):

```javascript theme={null}
// Extract the artifact (currently AdCP returns single artifact per response)
const artifact = response.artifacts?.[0];

if (artifact) {
  // Detect Part type by presence of field (1.0) with kind fallback (v0.3)
  const isText = (p) => typeof p.text === 'string' || p.kind === 'text';
  const isData = (p) => p.data != null || p.kind === 'data';

  const message = artifact.parts?.find(isText)?.text;
  const data = artifact.parts?.find(isData)?.data;

  return {
    artifactId: artifact.artifactId,
    message,
    data,
    status: response.status
  };
}

return { status: response.status };
```

**For complete response structure requirements, error handling, and implementation patterns, see [A2A Response Format](/docs/building/by-layer/L0/a2a-response-format).**

## Push Notifications (A2A-Specific)

A2A defines push notifications natively via `PushNotificationConfig`. When you configure a webhook URL, the server will POST task updates directly to your endpoint instead of requiring you to poll.

### Correlation: payload field, not URL

Correlate incoming notifications using `operation_id` (and `task_type`) from the payload body — **never** by parsing `pushNotificationConfig.url`. The URL is opaque to the server; the wire-level source of truth for correlation is the payload field. See [Webhooks — Operation IDs and URL templates](/docs/building/by-layer/L3/webhooks#operation-ids-and-url-templates) for the full normative wire contract (it applies to both MCP and A2A — every comparable async-notification protocol in ad tech makes the URL opaque to the firing entity).

Buyers MAY encode `operation_id` in the URL path or query as a routing aid for their own HTTP server — many web frameworks dispatch on path segments before parsing the body — but that's a buyer-side server design choice, not part of the wire contract. A buyer's server-routing template is not visible to the seller; the seller reads `operation_id` only from the buyer-supplied `pushNotificationConfig.operation_id` field and echoes it verbatim in the payload.

**URL templates (buyer-side server routing only):**

```javascript theme={null}
// Path parameters
url: `https://buyer.com/webhooks/a2a/${taskType}/${operationId}`

// Query parameters
url: `https://buyer.com/webhooks/a2a?task=${taskType}&op=${operationId}`

// Or fully opaque — the seller doesn't care about URL shape
url: `https://buyer.com/webhooks/${randomToken}`
```

**Example Configuration:**

```javascript theme={null}
const operationId = "op_nike_q1_2025";
const taskType = "create_media_buy";

await a2a.send({
  message: {
    role: "ROLE_USER",
    parts: [{
      data: {
        skill: "create_media_buy",
        parameters: { /* task params */ }
      }
    }]
  },
  pushNotificationConfig: {
    url: `https://buyer.com/webhooks/a2a/${taskType}/${operationId}`,
    operation_id: operationId,  // canonical correlation channel — seller echoes verbatim
    token: "client-validation-token",  // Optional: for client-side validation
    authentication: {
      schemes: ["bearer"],
      credentials: "shared_secret_32_chars"
    }
  }
});
```

For webhook payload formats, protocol comparison, and detailed handling examples, see [Webhooks](/docs/building/by-layer/L3/webhooks).

## SSE Streaming (A2A-Specific)

A2A's key advantage is real-time updates via Server-Sent Events:

AdCP still owns the application-layer task lifecycle on A2A. A2A `Task`, `taskId`, SSE, and push-notification frames are transport delivery mechanics; the durable business operation remains the AdCP payload keyed by AdCP `task_id`. A completed A2A task can still carry an AdCP response whose payload says `status: 'submitted'`.

### Task Monitoring

```javascript theme={null}
class A2aTaskMonitor {
  constructor(taskId) {
    this.taskId = taskId;
    this.events = new EventSource(`/a2a/tasks/${taskId}/events`);
    
    this.events.addEventListener('status', (e) => {
      const update = JSON.parse(e.data);
      this.handleStatusUpdate(update);
    });
    
    this.events.addEventListener('progress', (e) => {
      const data = JSON.parse(e.data);
      console.log(`${data.percentage}% - ${data.message}`);
    });
  }
  
  handleStatusUpdate(update) {
    switch (update.status) {
      case 'input-required':
        // Handle clarification/approval needed
        this.emit('input-required', update);
        break;
      case 'completed':
        this.events.close();
        this.emit('completed', update);
        break;
      case 'failed':
        this.events.close();
        this.emit('failed', update);
        break;
    }
  }
}
```

### Real-Time Updates Example

```javascript theme={null}
// Start long-running operation
const response = await a2a.send({
  message: {
    role: "ROLE_USER",
    parts: [{
      data: {
        skill: "create_media_buy",
        parameters: { packages: ["pkg_001"], total_budget: 100000 }
      }
    }]
  }
});

// Monitor A2A transport progress in real time via SSE
if (response.status === 'working' || response.status === 'submitted') {
  const monitor = new A2aTaskMonitor(response.taskId);
  
  monitor.on('progress', (data) => {
    updateUI(`${data.percentage}%: ${data.message}`);
  });
  
  monitor.on('completed', (final) => {
    // Extract last DataPart from the artifact — don't assume a positional index.
    const parts = final.artifacts[0].parts;
    const dataParts = parts.filter(p => p.data != null || p.kind === 'data');
    const payload = dataParts[dataParts.length - 1]?.data;
    if (payload?.status === 'submitted') {
      // A2A delivery completed, but the AdCP operation is still queued.
      return pollAdcpTask(payload.task_id);
    }
    console.log('Created:', payload?.media_buy_id);
  });
}
```

### A2A Webhook Payload Examples

**Example 1: `Task` payload for completed operation**

When a task finishes, the server sends the full `Task` object wrapped in the A2A 1.0 `StreamResponse` envelope. The task result lives in `.artifacts`:

```json theme={null}
{
  "task": {
    "id": "task_456",
    "contextId": "ctx_123",
    "status": {
      "state": "TASK_STATE_COMPLETED",
      "timestamp": "2026-01-22T10:30:00.000Z"
    },
    "artifacts": [{
      "name": "task_result",
      "parts": [
        {
          "text": "Media buy created successfully"
        },
        {
          "data": {
            "media_buy_id": "mb_12345",
            "creative_deadline": "2026-01-30T23:59:59.000Z",
            "packages": [
              {
                "package_id": "pkg_001",
                "context": { "line_item": "li_ctv_sports" }
              }
            ]
          }
        }
      ]
    }]
  }
}
```

**CRITICAL**: For **`completed`, `failed`, or `rejected`** status, the AdCP task result **MUST** be in `.artifacts[0].parts[]`. If the server has only a free-text fatal message (no structured payload), it MAY fall back to `status.message.parts[]` — clients handle both.

The A2A 1.0 `StreamResponse` oneof wraps every SSE frame and push-notification payload with exactly one of: `{ task }`, `{ statusUpdate }`, `{ artifactUpdate }`, `{ message }` (A2A 1.0 §3.2.3, §4.3.3). Non-streaming responses from `tasks/get` and v0.3 servers deliver the bare object. Clients unwrap before reading fields.

**Example 2: `TaskStatusUpdateEvent` for progress updates**

During execution, interim status updates can include optional data in `status.message.parts[]`. SSE/push frames wrap the event as `{ "statusUpdate": { … } }`:

```json theme={null}
{
  "statusUpdate": {
    "taskId": "task_456",
    "contextId": "ctx_123",
    "status": {
      "state": "TASK_STATE_INPUT_REQUIRED",
      "message": {
        "role": "ROLE_AGENT",
        "parts": [
          { "text": "Campaign budget $150K requires VP approval" },
          {
            "data": {
              "reason": "BUDGET_EXCEEDS_LIMIT"
            }
          }
        ]
      },
      "timestamp": "2026-01-22T10:15:00.000Z"
    }
  }
}
```

**All status payloads use AdCP schemas**: Both final statuses (completed/failed) and interim statuses (working, input-required, submitted) have corresponding AdCP schemas referenced in [`async-response-data.json`](https://adcontextprotocol.org/schemas/v3/core/async-response-data.json). Note that interim status schemas are evolving and may change in future versions, so implementors may choose to handle them more loosely.

### A2A Webhook Payload Types

Per the [A2A 1.0 specification](https://a2a-protocol.org/latest/specification/#433-push-notification-payload), the server sends different payload types wrapped in the `StreamResponse` oneof:

| Envelope Key     | Inner Payload             | When Used                                                                                       | What It Contains                                        |
| ---------------- | ------------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| `task`           | `Task`                    | Final states (`completed`, `failed`, `canceled`, `rejected`) or when full context needed        | Complete task object with all history and artifact data |
| `statusUpdate`   | `TaskStatusUpdateEvent`   | Status transitions during execution (`working`, `input-required`, `auth-required`, `submitted`) | Lightweight status change with message parts            |
| `artifactUpdate` | `TaskArtifactUpdateEvent` | Streaming artifact updates                                                                      | Artifact chunk with `append` / `lastChunk` flags        |
| `message`        | `Message`                 | Out-of-band agent messages                                                                      | A message unattached to a task status transition        |

For AdCP, most webhooks will be:

* `{ task }` for final results (`completed`, `failed`, `rejected`)
* `{ statusUpdate }` for progress updates (`working`, `input-required`, `auth-required`)

Clients unwrap the single-key envelope before reading fields. Non-streaming responses (e.g., `tasks/get`) deliver the bare payload — unwrapping a single-key envelope is a no-op there.

**Envelope semantics:**

* **`{ artifactUpdate }`** frames carry incremental artifact chunks with boolean flags `append` (concatenate parts onto the named artifact) and `lastChunk` (marks the final chunk). AdCP clients consuming streams SHOULD accumulate these into the target artifact, then apply the extraction algorithm when the `{ task }` frame arrives with a terminal state. Clients consuming push notifications typically receive the already-merged `Task` object and can ignore individual `artifactUpdate` frames. See A2A 1.0 §7.3.
* **`{ message }`** frames are out-of-band agent messages unattached to a task status transition. AdCP is task-oriented — task-facing clients SHOULD log and ignore bare `message` envelopes.

### Webhook Trigger Rules

Webhooks are sent when **all** of these conditions are met:

1. **Task type supports async** (e.g., `create_media_buy`, `sync_creatives`, `get_products`)
2. **`pushNotificationConfig` is provided** in the request
3. **Task runs asynchronously** — initial response is `working` or `submitted`

If the initial response is already terminal (`completed`, `failed`, `rejected`), no webhook is sent—you already have the result.

**Status changes that trigger webhooks:**

* `working` → Progress update (task actively processing)
* `input-required` → Human input needed
* `auth-required` (1.0) → Re-authentication challenge during execution
* `completed` → Final result available
* `failed` → Error details
* `rejected` (1.0) → Policy/validation rejection with `adcp_error`
* `canceled` → Cancellation confirmed

### Data Schema Validation

The DataPart `data` field in A2A webhooks uses status-specific schemas:

| Status                | Schema                                      | Contents                                      |
| --------------------- | ------------------------------------------- | --------------------------------------------- |
| `completed`           | `[task]-response.json`                      | Full task response (success branch)           |
| `failed`              | `[task]-response.json`                      | Full task response (error branch)             |
| `rejected` (1.0)      | `[task]-response.json` (error branch)       | Policy/validation rejection with `adcp_error` |
| `working`             | `[task]-async-response-working.json`        | Progress info (`percentage`, `step`)          |
| `input-required`      | `[task]-async-response-input-required.json` | Requirements, approval data                   |
| `auth-required` (1.0) | `[task]-async-response-auth-required.json`  | Auth challenge (scheme, URL, scopes)          |
| `submitted`           | `[task]-async-response-submitted.json`      | Acknowledgment (usually minimal)              |

Schema reference: [`async-response-data.json`](https://adcontextprotocol.org/schemas/v3/core/async-response-data.json)

### Webhook Handler Example

```javascript theme={null}
const express = require('express');
const app = express();

app.post('/webhooks/a2a/:taskType/:operationId', async (req, res) => {
  const { taskType, operationId } = req.params;
  const rawBody = req.body;

  // Verify webhook authenticity (Bearer token example)
  const authHeader = req.headers.authorization;
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Missing Authorization header' });
  }
  const token = authHeader.substring(7);
  if (token !== process.env.A2A_WEBHOOK_TOKEN) {
    return res.status(401).json({ error: 'Invalid token' });
  }

  // Unwrap A2A 1.0 StreamResponse envelope: { task } | { statusUpdate } | { artifactUpdate } | { message }
  const envelopeKeys = ['task', 'message', 'statusUpdate', 'artifactUpdate'];
  const bodyKeys = Object.keys(rawBody || {});
  const webhook = (bodyKeys.length === 1 && envelopeKeys.includes(bodyKeys[0]))
    ? rawBody[bodyKeys[0]]
    : rawBody;

  // Extract basic fields from A2A webhook payload
  const taskId = webhook.id || webhook.taskId;
  const contextId = webhook.contextId;
  const status = webhook.status?.state || webhook.status;

  // Normalize 1.0 / v0.3 state values
  const normalizeState = (s) => s?.replace(/^TASK_STATE_/, '').toLowerCase().replace(/_/g, '-');
  const normalizedStatus = normalizeState(status);

  // Detect Part type by field presence (1.0) with kind fallback (v0.3)
  const isDataPart = (p) => p.data != null || p.kind === 'data';
  const isTextPart = (p) => typeof p.text === 'string' || p.kind === 'text';

  // Extract AdCP data based on status
  let adcpData, textMessage;

  const FINAL = ['completed', 'failed', 'canceled', 'rejected'];

  if (FINAL.includes(normalizedStatus)) {
    // FINAL STATES: Extract from .artifacts (fallback to status.message.parts)
    const artifactParts = webhook.artifacts?.[0]?.parts;
    const dataPart = artifactParts?.find(isDataPart)
      ?? webhook.status?.message?.parts?.find(isDataPart);
    const textPart = artifactParts?.find(isTextPart)
      ?? webhook.status?.message?.parts?.find(isTextPart);
    adcpData = dataPart?.data;
    textMessage = textPart?.text;
  } else {
    // INTERIM STATES: Extract from status.message.parts (optional)
    const dataPart = webhook.status?.message?.parts?.find(isDataPart);
    const textPart = webhook.status?.message?.parts?.find(isTextPart);
    adcpData = dataPart?.data;
    textMessage = textPart?.text;
  }

  // Handle status changes (normalized works for both 1.0 and v0.3 wire values)
  switch (normalizedStatus) {
    case 'input-required':
      // Alert human that input is needed
      await notifyHuman({
        task_id: taskId,
        context_id: contextId,
        message: textMessage,
        data: adcpData
      });
      break;

    case 'auth-required':
      // A2A 1.0: re-authenticate and resume the task
      // SECURITY: validate challenge_url against the agent's registered origin
      // before opening/fetching. See A2A Response Extraction §Auth Challenge URL Validation.
      if (!isValidChallengeUrl(adcpData?.challenge_url, agentAuthOrigin(taskId))) {
        return res.status(400).json({ error: 'Invalid challenge_url for agent' });
      }
      await startAuthChallenge({
        task_id: taskId,
        auth_scheme: adcpData?.auth_scheme,
        challenge_url: adcpData.challenge_url,
        scopes: adcpData?.scopes  // show to user for fresh consent, do not auto-grant
      });
      break;

    case 'completed':
      // Process the completed operation
      if (adcpData?.media_buy_id) {
        await handleMediaBuyCreated({
          media_buy_id: adcpData.media_buy_id,
          packages: adcpData.packages
        });
      }
      break;

    case 'failed':
      // Handle failure
      await handleOperationFailed({
        task_id: taskId,
        error: adcpData?.adcp_error ?? adcpData?.errors,
        message: textMessage
      });
      break;

    case 'rejected':
      // A2A 1.0: policy/validation rejection with structured adcp_error
      await handleOperationRejected({
        task_id: taskId,
        error: adcpData?.adcp_error,
        message: textMessage
      });
      break;

    case 'working':
      // Update progress UI
      await updateProgress({
        task_id: taskId,
        percentage: adcpData?.percentage,
        message: textMessage
      });
      break;

    case 'canceled':
      await handleOperationCanceled(taskId);
      break;
  }

  // Always return 200 for successful processing
  res.status(200).json({ status: 'processed' });
});
```

## Context Management (A2A-Specific)

**Key Advantage**: A2A handles context automatically - no manual context\_id management needed.

### Automatic Context

```javascript theme={null}
// First request - A2A creates context automatically
const response1 = await a2a.send({
  message: {
    role: "ROLE_USER",
    parts: [{ text: "Find premium video products" }]
  }
});

// Follow-up - A2A remembers context automatically
const response2 = await a2a.send({
  message: {
    role: "ROLE_USER",
    parts: [{ text: "Filter for sports content" }]
  }
});
// System automatically connects this to previous request
```

### Explicit Context (Optional)

```javascript theme={null}
// When you need explicit control
const response2 = await a2a.send({
  contextId: response1.contextId,  // Optional - A2A tracks this anyway
  message: {
    role: "ROLE_USER",
    parts: [{ text: "Refine those results" }]
  }
});
```

**vs. MCP**: Unlike MCP's manual context\_id management, A2A handles session continuity at the protocol level.

## Multi-Modal Messages (A2A-Specific)

A2A's unique capability - combine text, data, and files in one message:

### Creative Upload with Context

```javascript theme={null}
// Upload creative with campaign context in single message
const response = await a2a.send({
  message: {
    role: "ROLE_USER",
    parts: [
      {
        text: "Add this hero video to the premium sports campaign"
      },
      {
        data: {
          skill: "sync_creatives",
          parameters: {
            media_buy_id: "mb_12345",
            action: "upload_and_assign"
          }
        }
      },
      {
        url: "https://cdn.example.com/hero-30s.mp4",
        filename: "sports_hero_30s.mp4",
        mediaType: "video/mp4"
      }
    ]
  }
});
```

### Campaign Brief + Assets

```javascript theme={null}
// Submit comprehensive campaign brief
await a2a.send({
  message: {
    role: "ROLE_USER",
    parts: [
      {
        text: "Campaign brief and assets for Q1 launch"
      },
      {
        url: "https://docs.google.com/campaign-brief.pdf",
        filename: "Q1_campaign_brief.pdf",
        mediaType: "application/pdf"
      },
      {
        data: {
          budget: 250000,
          kpis: ["reach", "awareness", "conversions"],
          target_launch: "2026-01-15"
        }
      }
    ]
  }
});
```

## Available Skills

All AdCP tasks are available as A2A skills. Use explicit invocation for deterministic execution:

**Task Management**: For comprehensive guidance on tracking async operations across all domains, polling patterns, and webhook integration, see [Webhooks](/docs/building/by-layer/L3/webhooks).

### Skill Structure

```javascript theme={null}
// Standard pattern for explicit skill invocation
await a2a.send({
  message: {
    role: "ROLE_USER",
    parts: [{
      data: {
        skill: "skill_name",        // Exact name from Agent Card
        parameters: {              // Task-specific parameters
          // See task documentation for parameters
        }
      }
    }]
  }
});
```

### Available Skills

* **Protocol**: `get_adcp_capabilities` (start here to discover agent capabilities)
* **Media Buy**: `get_products`, `list_creative_formats`, `create_media_buy`, `update_media_buy`, `sync_creatives`, `get_media_buy_delivery`, `provide_performance_feedback`
* **Signals**: `get_signals`, `activate_signal`

**Task Parameters**: See [Media Buy](/docs/media-buy) and [Signals](/docs/signals/overview) documentation for complete parameter specifications.

## Agent Cards

A2A agents advertise capabilities via Agent Cards at `.well-known/agent.json`.

### Discovering Agent Cards

```javascript theme={null}
// Get agent capabilities
const agentCard = await a2a.getAgentCard();

// List available skills
const skillNames = agentCard.skills.map(skill => skill.name);
console.log('Available skills:', skillNames);

// Get skill details
const getProductsSkill = agentCard.skills.find(s => s.name === 'get_products');
console.log('Examples:', getProductsSkill.examples);

// Pick a transport interface (1.0)
const jsonrpc = agentCard.supportedInterfaces?.find(
  i => i.protocolBinding === 'JSONRPC' && i.protocolVersion === '1.0'
);
console.log('Endpoint:', jsonrpc?.url);
```

### Sample Agent Card Structure (A2A 1.0)

In 1.0, the top-level `url` and `protocolVersion` fields from v0.3 are replaced by a `supportedInterfaces` array. Each entry advertises one transport binding and protocol version. `supportsAuthenticatedExtendedCard` moved to `capabilities.extendedAgentCard`.

```json theme={null}
{
  "name": "AdCP Media Buy Agent",
  "description": "AI-powered media buying agent",
  "version": "1.0.0",
  "securitySchemes": {
    "bearerAuth": {
      "type": "http",
      "scheme": "bearer"
    }
  },
  "security": [{"bearerAuth": []}],
  "supportedInterfaces": [
    {
      "url": "https://sales.example.com/a2a/jsonrpc",
      "protocolBinding": "JSONRPC",
      "protocolVersion": "1.0"
    }
  ],
  "defaultInputModes": ["text/plain", "application/json"],
  "defaultOutputModes": ["application/json"],
  "capabilities": {
    "streaming": true,
    "pushNotifications": true,
    "extendedAgentCard": false
  },
  "skills": [
    {
      "name": "get_products",
      "description": "Discover available advertising products",
      "examples": [
        "Find premium CTV inventory for sports fans",
        "Show me video products under $50 CPM"
      ]
    }
  ],
  "extensions": [
    {
      "uri": "https://adcontextprotocol.org/extensions/adcp",
      "description": "AdCP media buying protocol support",
      "required": false,
      "params": {
        "adcp_version": "2.6.0",
        "protocols_supported": ["media_buy"],
        "extensions_supported": ["sustainability"]
      }
    }
  ]
}
```

### Dual-Advertising for v0.3 Compatibility

Servers transitioning from v0.3 advertise both interfaces. Clients pick the version they understand:

```json theme={null}
{
  "supportedInterfaces": [
    {
      "url": "https://sales.example.com/a2a/jsonrpc",
      "protocolBinding": "JSONRPC",
      "protocolVersion": "1.0"
    },
    {
      "url": "https://sales.example.com/",
      "protocolBinding": "JSONRPC",
      "protocolVersion": "0.3"
    }
  ]
}
```

Python SDK servers must also pass `enable_v0_3_compat=True` when constructing routes — backward compatibility is not enabled by default. See the [A2A Python SDK 1.0 migration guide](https://github.com/a2aproject/a2a-python/blob/v1.0.0/docs/migrations/v1_0/README.md).

### AdCP Extension

<Note>
  **Recommended**: Use [`get_adcp_capabilities`](/docs/protocol/get_adcp_capabilities) for runtime capability discovery. The agent card extension provides static metadata for agent registries and discovery services.
</Note>

Include the AdCP extension in your agent card's `extensions` array to declare AdCP support programmatically.

The A2A protocol uses an `extensions` array where each extension has:

* **`uri`**: Extension identifier (use `https://adcontextprotocol.org/extensions/adcp`)
* **`description`**: Human-readable description of how you use AdCP
* **`required`**: Whether clients must support this extension (typically `false` for AdCP)
* **`params`**: AdCP-specific configuration (see schema below)

```javascript theme={null}
// Check if agent supports AdCP
const agentCard = await fetch('https://sales.example.com/.well-known/agent.json')
  .then(r => r.json());

// Find the AdCP extension in the extensions array
const adcpExt = agentCard.extensions?.find(
  ext => ext.uri === 'https://adcontextprotocol.org/extensions/adcp'
);

if (adcpExt) {
  console.log('AdCP Version:', adcpExt.params.adcp_version);
  console.log('Supported domains:', adcpExt.params.protocols_supported);
  // ["media_buy", "creative", "signals"]
  console.log('Typed extensions:', adcpExt.params.extensions_supported);
  // ["sustainability"]
}
```

**Extension Params**: The `adcp-extension.json` schema was used in v2 to describe these params, but was removed in v3. For v3+ agents, use the `get_adcp_capabilities` task for runtime capability discovery instead. The extension `params` object above shows the typical structure.

:::note
The `adcp_version` field in agent card metadata is a v2 convention and is not part of the v3 spec. For v3 version negotiation, the buyer sends release-precision `adcp_version` (e.g., `"3.1"`) on every request, and the seller advertises supported releases via `adcp.supported_versions` on [`get_adcp_capabilities`](/docs/protocol/get_adcp_capabilities) and echoes `adcp_version` at the envelope root on every response. The legacy integer-only `adcp_major_version` field is still accepted for backwards compatibility. See [versioning.mdx § Version negotiation](/docs/reference/versioning#version-negotiation) for the full contract.
:::

**Benefits**:

* Clients can discover AdCP capabilities without making test calls
* Declare which protocol domains you implement (media\_buy, creative, signals)
* Enable compatibility checks based on version

## Integration Example

```javascript theme={null}
// Initialize A2A client  
const a2a = new A2AClient({ /* config */ });

// Use unified status handling (see Core Concepts)
async function handleA2aResponse(response) {
  switch (response.status) {
    case 'input-required':
      // Handle clarification (see Core Concepts for patterns)
      const input = await promptUser(response.message);
      return a2a.send({
        contextId: response.contextId,
        message: {
          role: "ROLE_USER",
          parts: [{ text: input }]
        }
      });

    case 'working':
      // Monitor via SSE streaming
      return streamUpdates(response.taskId);

    case 'completed':
      // Extract last DataPart — presence of .data field identifies it in 1.0
      const parts = response.artifacts[0].parts;
      const dataParts = parts.filter(p => p.data != null || p.kind === 'data');
      return dataParts[dataParts.length - 1].data;

    case 'failed':
      throw new Error(response.message);
  }
}

// Example usage with multi-modal message
const result = await a2a.send({
  message: {
    role: "ROLE_USER",
    parts: [
      { text: "Find luxury car inventory" },
      { data: { skill: "get_products", parameters: { audience: "luxury car intenders" } } }
    ]
  }
});

const finalResult = await handleA2aResponse(result);
```

## A2A-Specific Considerations

### Error Handling

Failed tasks carry structured AdCP errors in artifact `DataPart` under the `adcp_error` key. For the full extraction logic and recovery behavior, see [Transport Error Mapping](/docs/building/operating/transport-errors).

```javascript theme={null}
try {
  const response = await a2a.send(message);

  if (response.status === 'failed') {
    // Check for structured AdCP error in artifacts
    // Detect DataPart by field presence (1.0) or kind (v0.3)
    const dataPart = response.artifacts?.[0]?.parts?.find(
      p => p.data != null || p.kind === 'data'
    );
    const adcpError = dataPart?.data?.adcp_error;

    if (adcpError) {
      // Structured error with code, recovery, retry_after, etc.
      console.log('AdCP error:', adcpError.code, adcpError.recovery);
      if (adcpError.recovery === 'transient') {
        // Retry after delay
        await sleep((adcpError.retry_after || 5) * 1000);
        return retry();
      }
    }
    throw new Error(response.message);
  }
} catch (a2aError) {
  // A2A transport error (connection, auth, etc.)
  console.error('A2A Error:', a2aError);
}
```

### Creative Upload Error Handling

For uploading creative assets and handling validation errors, use the `sync_creatives` task. See [sync\_creatives Task Reference](/docs/creative/task-reference/sync_creatives) for complete testable examples.

The `@adcp/sdk` library handles A2A artifact extraction automatically, so you don't need to manually parse the response structure.

## Best Practices

1. **Use hybrid messages** for best results (text + data + optional files)
2. **Check status field** before processing artifacts
3. **Leverage SSE streaming** for real-time updates on long operations
4. **Reference Core Concepts** for status handling patterns
5. **Use agent cards** to discover available skills and examples

## Next Steps

* **Core Concepts**: Read [Task Lifecycle](/docs/building/by-layer/L3/task-lifecycle) for status handling and workflows
* **Task Reference**: See [Media Buy Tasks](/docs/media-buy) and [Signals](/docs/signals/overview)
* **Protocol Comparison**: Compare with [MCP integration](/docs/building/by-layer/L0/mcp-guide)
* **Examples**: Find complete workflow examples in Core Concepts

**For status handling, async operations, and clarification patterns, see [Task Lifecycle](/docs/building/by-layer/L3/task-lifecycle) - this guide focuses on A2A transport specifics only.**
