Skip to main content

Documentation Index

Fetch the complete documentation index at: https://agenticadvertisingorg-changeset-release-main.mintlify.app/llms.txt

Use this file to discover all available pages before exploring further.

Transport-specific guide for integrating AdCP using the Agent-to-Agent Protocol. For task handling, status management, and workflow patterns, see Task Lifecycle.

A2A Protocol Versions

AdCP tracks the A2A specification 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

Areav0.31.0
Agent Card transporturl + protocolVersion at rootsupportedInterfaces[] array with per-interface url, protocolBinding, protocolVersion
Part discriminatorkind: "text" | "data" | "file"No kind β€” content determined by which field is set (text, data, url, raw)
File fieldsuri, name, mimeTypeurl (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", …
TimestampsISO-8601ISO-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) accept both shapes during the compatibility period.

A2A Client Setup

1. Initialize A2A Client

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

// 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

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:
// 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)

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

Explicit Skill (Deterministic)

// 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"
        }
      }
    }]
  }
});
// 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 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.
{
  "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.

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)

Processing Artifacts

AdCP responses use the last DataPart as authoritative when multiple data parts exist (e.g., from streaming operations):
// 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.

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 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):
// 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:
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.

SSE Streaming (A2A-Specific)

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

Task Monitoring

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

// 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 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;
    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:
{
  "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": { … } }:
{
  "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. 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, the server sends different payload types wrapped in the StreamResponse oneof:
Envelope KeyInner PayloadWhen UsedWhat It Contains
taskTaskFinal states (completed, failed, canceled, rejected) or when full context neededComplete task object with all history and artifact data
statusUpdateTaskStatusUpdateEventStatus transitions during execution (working, input-required, auth-required, submitted)Lightweight status change with message parts
artifactUpdateTaskArtifactUpdateEventStreaming artifact updatesArtifact chunk with append / lastChunk flags
messageMessageOut-of-band agent messagesA 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:
StatusSchemaContents
completed[task]-response.jsonFull task response (success branch)
failed[task]-response.jsonFull task response (error branch)
rejected (1.0)[task]-response.json (error branch)Policy/validation rejection with adcp_error
working[task]-async-response-working.jsonProgress info (percentage, step)
input-required[task]-async-response-input-required.jsonRequirements, approval data
auth-required (1.0)[task]-async-response-auth-required.jsonAuth challenge (scheme, URL, scopes)
submitted[task]-async-response-submitted.jsonAcknowledgment (usually minimal)
Schema reference: async-response-data.json

Webhook Handler Example

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

// 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)

// 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

// 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

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

Skill Structure

// 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 and Signals documentation for complete parameter specifications.

Agent Cards

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

Discovering Agent Cards

// 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.
{
  "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:
{
  "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.

AdCP Extension

Recommended: Use get_adcp_capabilities for runtime capability discovery. The agent card extension provides static metadata for agent registries and discovery services.
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)
// 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 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 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

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

For status handling, async operations, and clarification patterns, see Task Lifecycle - this guide focuses on A2A transport specifics only.