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.
This document defines the canonical structure for AdCP responses transmitted over the A2A protocol.
Examples below use A2A 1.0 wire format: Parts carry no kind discriminator (content type is implied by which field is set — text, data, url, or raw), roles are ROLE_USER / ROLE_AGENT, and task states are TASK_STATE_* (ProtoJSON canonical). See the A2A Guide for a side-by-side with v0.3.
AdCP’s top-level unified status field (returned by @adcp/sdk) continues to use the lowercase shorthand ("completed", "failed", "working", "input-required", "submitted"). That is an AdCP abstraction over status.state — not an A2A wire value.
For v0.3 servers, the same DataPart becomes { "kind": "data", "data": {...} } and states become lowercase. Extraction clients accept both shapes during the compatibility period.
Required Structure
Final Responses (status: “completed”)
AdCP responses over A2A MUST:
- Include at least one DataPart (a Part carrying a non-null
data field) containing the task response payload
- Use single artifact with multiple parts (not multiple artifacts)
- Use the last DataPart as authoritative when multiple data parts exist
- NOT wrap AdCP payloads in custom framework objects (no
{ response: {...} } wrappers)
Recommended pattern:
{
"status": "completed",
"taskId": "task_123",
"contextId": "ctx_456",
"artifacts": [{
"name": "task_result",
"parts": [
{
"text": "Found 12 video products perfect for pet food campaigns"
},
{
"data": {
"products": [...],
"total": 12
}
}
]
}]
}
- TextPart (Part with
text field): Human-readable summary — recommended but optional
- DataPart (Part with
data field): Structured AdCP response payload — required
- FilePart (Part with
url or raw field): Optional file references (previews, reports)
Multiple artifacts: Only for fundamentally distinct deliverables (e.g., creative asset + separate trafficking report). Rare in AdCP - prefer single artifact with multiple parts.
Interim status updates are delivered as TaskStatusUpdateEvent, with optional progress/challenge data carried in status.message.parts[] (not in artifacts). Artifacts accumulate during the task lifecycle but are read as the final deliverable once the task reaches a terminal state.
{
"taskId": "task_123",
"contextId": "ctx_456",
"status": {
"state": "TASK_STATE_WORKING",
"timestamp": "2026-01-22T10:15:00.000Z",
"message": {
"role": "ROLE_AGENT",
"parts": [
{
"text": "Processing your request. Analyzing 50,000 inventory records..."
},
{
"data": {
"percentage": 45,
"current_step": "analyzing_inventory"
}
}
]
}
}
}
When delivered over SSE or as a push notification, this event is wrapped in the A2A 1.0 StreamResponse oneof: { "statusUpdate": { … } }. Non-streaming responses (e.g. tasks/get) deliver the bare object. Clients unwrap before reading status.state — see A2A Response Extraction.
Interim response characteristics:
- TextPart is recommended for human-readable status
- DataPart is optional but follows AdCP schemas when provided
- Interim status schemas (
*-async-response-working.json, *-async-response-input-required.json, etc.) are work-in-progress and may evolve
- Implementors may choose to handle interim data more loosely given schema evolution
When final status is reached (completed, failed, canceled, or rejected), the full AdCP task response is delivered on a Task object with the DataPart in .artifacts[0].parts[].
Framework Wrappers (NOT PERMITTED)
CRITICAL: DataPart content MUST be the direct AdCP response payload, not wrapped in framework-specific objects.
// ❌ WRONG - Wrapped in custom object
{
"data": {
"response": { // ← Framework wrapper
"products": [...]
}
}
}
// ✅ CORRECT - Direct AdCP payload
{
"data": {
"products": [...] // ← Direct schema-compliant response
}
}
Why this matters:
- Breaks schema validation (clients expect
products at root, not response.products)
- Adds unnecessary nesting layer
- Violates protocol-agnostic design (wrapper is framework-specific)
- Complicates client extraction code
If your implementation adds wrappers, this is a bug that should be fixed in the framework layer, not worked around in client code.
Canonical Client Behavior
This section defines EXACTLY how clients MUST extract AdCP responses from A2A protocol responses.
Quick Reference
| Status | Webhook Type | Data Location | Schema Required? | Returns |
|---|
working | TaskStatusUpdateEvent | status.message.parts[] | ✅ Yes (if present) | { status, taskId, message, data? } |
submitted | TaskStatusUpdateEvent | status.message.parts[] | ✅ Yes (if present) | { status, taskId, message, data? } |
input-required | TaskStatusUpdateEvent | status.message.parts[] | ✅ Yes (if present) | { status, taskId, message, data? } |
auth-required (1.0) | TaskStatusUpdateEvent | status.message.parts[] | ✅ Yes (auth challenge) | { status, taskId, message, data } |
completed | Task | .artifacts[] (fallback: status.message.parts[]) | ✅ Required | { status, taskId, message, data } |
failed | Task | .artifacts[] (fallback: status.message.parts[]) | ✅ Required | { status, taskId, message, data } |
rejected (1.0) | Task | .artifacts[] | ✅ Required (adcp_error) | { status, taskId, message, data } |
Key Insights:
- Final statuses use
Task object with data in .artifacts. If a server has no structured payload (e.g., JSON-RPC parse error, pre-task auth failure), it may place only a text message in status.message.parts — clients fall back to that location.
- Interim statuses use
TaskStatusUpdateEvent with optional data in status.message.parts[].
- Stream/webhook delivery wraps the payload in the A2A 1.0
StreamResponse oneof ({ task }, { statusUpdate }, { artifactUpdate }, { message }). Clients unwrap before reading fields.
- All statuses use AdCP schemas when data is present.
- Interim status schemas are work-in-progress and may evolve.
Rule 1: Status-Based Handling
Clients MUST branch on the normalized status to determine the correct data extraction location. The status referenced here is AdCP’s unified lowercase value (e.g. "completed"); the raw A2A wire value at status.state is TASK_STATE_COMPLETED in 1.0 or completed in v0.3. Normalize before comparing — see A2A Response Extraction.
const INTERIM = ['working', 'submitted', 'input-required', 'auth-required'];
const FINAL = ['completed', 'failed', 'canceled', 'rejected'];
function handleA2aResponse(response) {
const status = response.status; // AdCP unified status
// INTERIM STATUSES - Extract from status.message.parts (TaskStatusUpdateEvent)
if (INTERIM.includes(status)) {
return {
status: status,
taskId: response.taskId,
contextId: response.contextId,
message: extractTextPartFromMessage(response),
data: extractDataPartFromMessage(response), // Optional AdCP data (required for auth-required)
};
}
// FINAL STATUSES - Extract from .artifacts (Task object), fallback to status.message
if (FINAL.includes(status)) {
return {
status: status,
taskId: response.taskId,
contextId: response.contextId,
message: extractTextPartFromArtifacts(response) ?? extractTextPartFromMessage(response),
data: extractDataPartFromArtifacts(response) ?? extractDataPartFromMessage(response),
};
}
// Forward-compatible: unknown future states return null, do not throw
return { status, taskId: response.taskId, contextId: response.contextId, message: null, data: null };
}
Critical:
- Interim statuses use
TaskStatusUpdateEvent → extract from status.message.parts[]
- Final statuses use
Task object → extract from .artifacts[0].parts[], falling back to status.message.parts[] if artifacts are empty
Extract data from the appropriate location based on webhook type:
// Part-type detectors: field presence (A2A 1.0) with kind fallback (v0.3)
const isDataPart = (p) =>
p.data != null && typeof p.data === 'object' && !Array.isArray(p.data);
const isTextPart = (p) => typeof p.text === 'string';
// For FINAL statuses (Task object) - extract from .artifacts, return null if absent
function extractDataPartFromArtifacts(response) {
const dataParts = response.artifacts?.[0]?.parts?.filter(isDataPart) || [];
if (dataParts.length === 0) return null; // caller falls back to status.message.parts
// Use LAST data part as authoritative
const lastDataPart = dataParts[dataParts.length - 1];
const payload = lastDataPart.data;
// CRITICAL: Payload MUST be direct AdCP response, not a framework wrapper.
// A wrapper is a single-key object { response: {...} } — reject it.
// Objects that have 'response' alongside other keys are NOT wrappers.
const keys = Object.keys(payload);
if (keys.length === 1 && keys[0] === 'response' && typeof payload.response === 'object') {
throw new Error(
'Invalid response format: DataPart contains wrapper object. ' +
'Expected direct AdCP payload (e.g., {products: [...]}) ' +
'but received {response: {products: [...]}}. ' +
'This is a server-side bug that must be fixed.'
);
}
return payload;
}
function extractTextPartFromArtifacts(response) {
const textPart = response.artifacts?.[0]?.parts?.find(isTextPart);
return textPart?.text || null;
}
// For INTERIM statuses (TaskStatusUpdateEvent) - extract from status.message.parts
function extractDataPartFromMessage(response) {
const dataPart = response.status?.message?.parts?.find(isDataPart);
return dataPart?.data || null;
}
function extractTextPartFromMessage(response) {
const textPart = response.status?.message?.parts?.find(isTextPart);
return textPart?.text || null;
}
These detectors work for both wire formats: a 1.0 DataPart has data set (no kind), a v0.3 DataPart has kind: "data" and data set — both satisfy p.data != null.
Rule 3: Schema Validation
All AdCP responses use schemas, but validation approach varies by status:
function validateResponse(response, taskName) {
const status = response.status;
let data, schemaName;
// Extract data and determine schema based on status
if (INTERIM.includes(status)) {
// INTERIM: Optional data from status.message.parts
data = extractDataPartFromMessage(response);
if (data) {
// Interim status has its own schema (work-in-progress)
schemaName = `${taskName}-async-response-${status}.json`;
// Optional: Implementors may skip interim validation as schemas evolve
if (STRICT_VALIDATION_MODE) {
validateAgainstSchema(data, loadSchema(schemaName));
}
}
} else if (FINAL.includes(status)) {
// FINAL: Required data from .artifacts (fallback to status.message.parts)
data = extractDataPartFromArtifacts(response) ?? extractDataPartFromMessage(response);
schemaName = `${taskName}-response.json`;
// Required: Final responses must validate
if (!validateAgainstSchema(data, loadSchema(schemaName))) {
throw new Error(
`Response payload does not match ${taskName} schema. ` +
`Ensure DataPart contains direct AdCP response structure.`
);
}
}
}
Schema Evolution Note: Interim status schemas (*-async-response-working.json, etc.) are work-in-progress. Implementors may choose to handle these more loosely while schemas stabilize.
Complete Example
Putting it all together with proper handling of both Task and TaskStatusUpdateEvent payloads:
async function executeTask(taskName, params) {
const response = await a2aClient.send({
task: taskName,
params: params
});
// 1. Status-based handling (extracts from correct location)
const result = handleA2aResponse(response);
// 2. Schema validation
validateResponse(response, taskName);
return result;
}
// Usage
const result = await executeTask('get_products', {
brief: 'CTV inventory in California'
});
// Handle different response types
if (result.status === 'working') {
// TaskStatusUpdateEvent - data from status.message.parts
console.log('Processing:', result.message);
if (result.data) {
console.log('Progress:', result.data.percentage + '%');
}
} else if (result.status === 'input-required') {
// TaskStatusUpdateEvent - data from status.message.parts
console.log('Input needed:', result.message);
console.log('Reason:', result.data?.reason);
} else if (result.status === 'completed') {
// Task object - data from .artifacts
console.log('Success:', result.message);
console.log('Products:', result.data.products); // Full AdCP response
}
Last Data Part Authority Pattern
Test Cases
✅ Correct Behavior
// Test 1: Working status (TaskStatusUpdateEvent) - extract from status.message.parts
const workingResponse = {
taskId: 'task_123',
contextId: 'ctx_456',
status: {
state: 'TASK_STATE_WORKING',
message: {
role: 'ROLE_AGENT',
parts: [
{ text: 'Processing inventory...' },
{ data: { percentage: 50, current_step: 'analyzing' } }
]
}
}
};
const result1 = handleA2aResponse(workingResponse);
assert(result1.data.percentage === 50, 'Should extract data from status.message.parts');
assert(result1.message === 'Processing inventory...', 'Should extract text from status.message.parts');
// Test 2: Completed status (Task) - extract from .artifacts
const completedResponse = {
taskId: 'task_123',
contextId: 'ctx_456',
status: {
state: 'TASK_STATE_COMPLETED',
timestamp: '2026-01-22T10:30:00.000Z'
},
artifacts: [{
parts: [
{ text: 'Found 3 products' },
{ data: { products: [...], total: 3 } }
]
}]
};
const result2 = handleA2aResponse(completedResponse);
assert(result2.data !== undefined, 'Completed status must have data');
assert(Array.isArray(result2.data.products), 'Data should be direct AdCP payload');
// Test 3: Wrapper detection (should reject)
const wrappedResponse = {
taskId: 'task_123',
status: { state: 'TASK_STATE_COMPLETED' },
artifacts: [{
parts: [
{ data: { response: { products: [...] } } }
]
}]
};
assert.throws(() => {
extractDataPartFromArtifacts(wrappedResponse);
}, /Invalid response format.*wrapper/);
❌ Incorrect Behavior (Common Mistakes)
// WRONG: Extracting from wrong location for interim status
function badHandleWorking(response) {
// ❌ TaskStatusUpdateEvent doesn't have .artifacts - data is in status.message.parts
const data = response.artifacts?.[0]?.parts?.find(isDataPart)?.data;
return { status: 'working', data }; // Will be null/undefined!
}
// WRONG: Extracting from wrong location for completed status
function badHandleCompleted(response) {
// ❌ Task object has data in .artifacts, not in status.message.parts
const data = response.status?.message?.parts?.find(p => p.data)?.data;
return { status: 'completed', data }; // Will be null/undefined!
}
// WRONG: Not checking for wrappers
function badExtraction(response) {
const payload = response.artifacts[0].parts[0].data;
// ❌ Returns { response: { products: [...] } } instead of { products: [...] }
return payload; // Client receives wrong structure!
}
// WRONG: Accessing nested response field
function badClientUsage(result) {
// ❌ Client code shouldn't need to do this
const products = result.data.response.products;
// Should be: result.data.products
}
Error Handling
Task-Level Errors (Partial Failures)
Task executed but couldn’t complete fully. Use errors array in DataPart with status: "completed":
{
"status": "completed",
"taskId": "task_123",
"artifacts": [{
"parts": [
{
"text": "Signal discovery completed with partial results"
},
{
"data": {
"signals": [...],
"errors": [{
"code": "NO_DATA_IN_REGION",
"message": "No signal data available for Australia",
"field": "countries[1]",
"details": {
"requested_country": "AU",
"available_countries": ["US", "CA", "GB"]
}
}]
}
}
]
}]
}
When to use errors array:
- Platform authorization issues (
PLATFORM_UNAUTHORIZED)
- Partial data availability
- Validation issues in subset of data
Protocol-Level Errors (Fatal)
Task couldn’t execute. Use status: "failed" with message:
{
"taskId": "task_456",
"status": "failed",
"message": {
"role": "ROLE_AGENT",
"parts": [{
"text": "Authentication failed: Invalid or expired API token"
}]
}
}
When to use status: failed:
- Authentication failures (invalid credentials, expired tokens)
- Invalid request parameters (malformed JSON, missing required fields)
- Resource not found (unknown taskId, expired context)
- System errors (database unavailable, internal service failure)
Where the Error Lives: Decision Rule
Placement is chosen by what the server has and which state it’s in:
| Situation | State | Location | Payload |
|---|
| Task executed, subset failed | completed | artifacts[0].parts[] DataPart | { <success_fields>, errors: [...] } |
| Task failed with structured error | failed | artifacts[0].parts[] DataPart | { adcp_error: {...} } |
| Task rejected by policy/validation (1.0) | rejected | artifacts[0].parts[] DataPart | { adcp_error: {...} } |
| System-initiated cancel (timeout, upstream failure) | canceled | artifacts[0].parts[] DataPart | { adcp_error: {...} } |
User-initiated cancel (tasks/cancel) | canceled | status.message.parts[] TextPart | Human-readable text only |
| Protocol/transport failure, no artifact produced | failed | status.message.parts[] TextPart | Human-readable text only |
Rule of thumb: if the server has structured error data, put it in artifacts as a DataPart. status.message is the free-text fallback for cases where no task artifact was ever produced (JSON-RPC parse errors, auth handshake failures, malformed requests, or a user-initiated cancel with no further detail). A2A 1.0 §3.7 reinforces this: “Messages SHOULD NOT be used to deliver task outputs. Results SHOULD be returned using Artifacts.”
rejected vs failed. Use rejected when the server refuses to attempt the task (policy/tier/validation check, before any work is started). Use failed when work started and encountered a fatal error. Both carry adcp_error in the artifact — the state distinguishes when the failure occurred, which drives different retry and UX behavior on the caller side.
Cancel origin is client-reconciled, not seller-attributed. status.state: "canceled" (or TASK_STATE_CANCELED) does not tell the caller whether the cancel was user-initiated or system-initiated — a seller could place adcp_error in artifacts for what was actually a user-initiated cancel to mislead the buyer’s bookkeeping or retry logic. Clients MUST reconcile cancel origin locally: if the caller has an outstanding tasks/cancel request for this taskId, treat the cancel as user-initiated regardless of payload and ignore any adcp_error the seller attached. Clients MUST NOT retry a user-initiated cancel on the basis of a seller-sent adcp_error.recovery hint.
Status Mapping
AdCP uses A2A’s TaskState enum directly:
| A2A Status | Payload Type | Data Location | AdCP Usage |
|---|
completed | Task | .artifacts | Task finished successfully, data in DataPart, optional errors array |
failed | Task | .artifacts (or status.message for text-only) | Fatal error preventing completion, adcp_error when structured |
rejected (1.0) | Task | .artifacts | Policy/validation rejection, adcp_error with rejection reason |
canceled | Task | .artifacts (typically none) | Task canceled by user or system |
input-required | TaskStatusUpdateEvent | status.message.parts | Need user input/approval, data + text explaining what’s needed |
auth-required (1.0) | TaskStatusUpdateEvent | status.message.parts | Authentication challenge during task execution (scheme, URL, scopes) |
working | TaskStatusUpdateEvent | status.message.parts | Processing (< 120s), optional progress data |
submitted | TaskStatusUpdateEvent | status.message.parts | Long-running (hours/days), minimal data, use webhooks/polling |
Webhook Payloads
Async operations (status: "submitted") deliver the same artifact structure in webhooks:
POST /webhook-endpoint
{
"taskId": "task_123",
"status": "completed",
"timestamp": "2026-01-22T10:30:00.000Z",
"artifacts": [{
"parts": [
{"text": "Media buy approved and live"},
{"data": {
"media_buy_id": "mb_456",
"packages": [...],
"creative_deadline": "2026-01-30T23:59:59.000Z"
}}
]
}]
}
Extract AdCP data using the same last-DataPart pattern. For webhook authentication, retry patterns, and security, see Webhooks.
File Parts in Responses
Creative operations MAY include file references:
{
"status": "completed",
"artifacts": [{
"parts": [
{"text": "Creative uploaded and preview generated"},
{"data": {
"creative_id": "cr_789",
"format_id": {
"agent_url": "https://creatives.adcontextprotocol.org",
"id": "video_standard_30s"
},
"status": "ready"
}},
{"url": "https://cdn.example.com/cr_789/preview.mp4", "filename": "preview.mp4", "mediaType": "video/mp4"}
]
}]
}
File part usage: Preview URLs, generated assets, trafficking reports. Not for raw AdCP response data (always use DataPart).
Retry and Idempotency
TaskId-Based Deduplication
A2A’s taskId enables retry detection. Agents SHOULD:
- Return cached response if
taskId matches a completed operation (within TTL window)
- Reject duplicate
taskId submission if operation is still in progress
// Duplicate taskId during active operation
{
"taskId": "task_123",
"status": "failed",
"message": {
"role": "ROLE_AGENT",
"parts": [{
"text": "Task 'task_123' is already in progress. Use tasks/get to check status."
}]
}
}
Examples
Implementation Checklist
When implementing A2A responses for AdCP:
Final Responses (status: “completed” or “failed”) - Use Task object:
Interim Responses (status: “working”, “submitted”, “input-required”) - Use TaskStatusUpdateEvent:
Error Handling:
General:
See Also