diff --git a/.changeset/refactor-push-notification-config.md b/.changeset/refactor-push-notification-config.md
new file mode 100644
index 000000000..2dc407fcb
--- /dev/null
+++ b/.changeset/refactor-push-notification-config.md
@@ -0,0 +1,40 @@
+---
+"adcontextprotocol": patch
+---
+
+Redesign how AdCP handles push notifications for async tasks. The key change is separating **what data is sent** (AdCP's responsibility) from **how it's delivered** (protocol's responsibility).
+
+**Renamed:**
+
+- `webhook-payload.json` → `mcp-webhook-payload.json` (clarifies this envelope is MCP-specific)
+
+**Created:**
+
+- `async-response-data.json` - Union schema for all async response data types
+- Status-specific schemas for `working`, `input-required`, and `submitted` statuses
+
+**Deleted:**
+
+- Removed redundant `-async-response-completed.json` and `-async-response-failed.json` files (6 total)
+- For `completed`/`failed`, we now use the existing task response schemas directly
+
+**Before:** The webhook spec tried to be universal, which created confusion about how A2A's native push notifications fit in.
+
+**After:**
+
+- MCP uses `mcp-webhook-payload.json` as its envelope, with AdCP data in `result`
+- A2A uses its native `Task`/`TaskStatusUpdateEvent` messages, with AdCP data in `status.message.parts[].data`
+- Both use the **exact same data schemas** - only the envelope differs
+
+This makes it clear that AdCP only specifies the data layer, while each protocol handles delivery in its own way.
+
+**Schemas:**
+
+- `static/schemas/source/core/mcp-webhook-payload.json` (renamed + simplified)
+- `static/schemas/source/core/async-response-data.json` (new)
+- `static/schemas/source/media-buy/*-async-response-*.json` (6 deleted, 9 remain)
+
+- Clarified that both MCP and A2A use HTTP webhooks (A2A's is native to the spec, MCP's is AdCP-provided)
+- Fixed webhook trigger rules: webhooks fire for **all status changes** if `pushNotificationConfig` is provided and the task runs async
+- Added proper A2A webhook payload examples (`Task` vs `TaskStatusUpdateEvent`)
+- **Task Management** added to sidebar, it was missing
diff --git a/docs.json b/docs.json
index 61c07c1e7..ddc62cdac 100644
--- a/docs.json
+++ b/docs.json
@@ -63,8 +63,9 @@
]
},
"docs/protocols/protocol-comparison",
- "docs/protocols/envelope-examples",
- "docs/protocols/context-management"
+ "docs/protocols/context-management",
+ "docs/protocols/task-management",
+ "docs/protocols/envelope-examples"
]
},
{
diff --git a/docs/media-buy/task-reference/create_media_buy.mdx b/docs/media-buy/task-reference/create_media_buy.mdx
index 78ce48cfc..930214904 100644
--- a/docs/media-buy/task-reference/create_media_buy.mdx
+++ b/docs/media-buy/task-reference/create_media_buy.mdx
@@ -582,46 +582,359 @@ Invalid format example:
## Asynchronous Operations
-This operation can complete instantly or take days depending on complexity and approval requirements.
+This task can complete instantly or take days depending on complexity and approval requirements. The response includes a `status` field that tells you what happened and what to do next.
-### Response Patterns
+| Status | Meaning | Your Action |
+|--------|---------|-------------|
+| `completed` | Done immediately | Process the result |
+| `working` | Processing (~2 min) | Poll frequently or wait for webhook |
+| `submitted` | Long-running (hours/days) | Use webhooks or poll infrequently |
+| `input-required` | Needs your input | Read message, respond with info |
+| `failed` | Error occurred | Handle the error |
-**Synchronous (completed immediately)**:
+**Note:** For the complete status list see [Core Concepts - Task Status System](/docs/protocols/core-concepts#task-status-system).
+
+
+
+
+### Immediate Success (`completed`)
+
+The task completed synchronously. No async handling needed.
+
+**Request:**
+```javascript
+const response = await session.call('create_media_buy', {
+ buyer_ref: 'summer_campaign_2025',
+ brand_manifest: { name: 'Nike', url: 'https://nike.com' },
+ packages: [
+ {
+ buyer_ref: 'ctv_package',
+ product_id: 'prod_ctv_sports',
+ pricing_option_id: 'cpm_fixed',
+ budget: 50000
+ }
+ ]
+});
+```
+
+**Response:**
```json
{
+ "status": "completed",
"media_buy_id": "mb_12345",
"buyer_ref": "summer_campaign_2025",
- "packages": []
+ "creative_deadline": "2025-06-15T23:59:59Z",
+ "packages": [
+ {
+ "package_id": "pkg_001",
+ "buyer_ref": "ctv_package",
+ "product_id": "prod_ctv_sports"
+ }
+ ]
+}
+```
+
+### Long-Running (`submitted`)
+
+The task is queued for manual approval. Configure a webhook to receive updates.
+
+**Request with webhook:**
+```javascript
+const response = await session.call('create_media_buy',
+ {
+ buyer_ref: 'enterprise_campaign',
+ brand_manifest: { name: 'Nike', url: 'https://nike.com' },
+ packages: [
+ {
+ buyer_ref: 'premium_package',
+ product_id: 'prod_premium_ctv',
+ pricing_option_id: 'cpm_fixed',
+ budget: 500000 // Large budget triggers approval
+ }
+ ]
+ },
+ {
+ pushNotificationConfig: {
+ url: 'https://your-app.com/webhooks/adcp',
+ authentication: {
+ schemes: ['bearer'],
+ credentials: 'your_webhook_secret'
+ }
+ }
+ }
+);
+```
+
+**Initial response:**
+```json
+{
+ "status": "submitted",
+ "task_id": "task_abc123",
+ "message": "Budget exceeds auto-approval limit. Sales review required (2-4 hours)."
+}
+```
+
+**Webhook POST when approved:**
+```json
+{
+ "task_id": "task_abc123",
+ "task_type": "create_media_buy",
+ "status": "completed",
+ "timestamp": "2025-01-22T14:30:00Z",
+ "message": "Media buy approved and created",
+ "result": {
+ "media_buy_id": "mb_67890",
+ "buyer_ref": "enterprise_campaign",
+ "creative_deadline": "2025-06-20T23:59:59Z",
+ "packages": [
+ {
+ "package_id": "pkg_002",
+ "buyer_ref": "premium_package"
+ }
+ ]
+ }
+}
+```
+
+### Error (`failed`)
+
+**Response:**
+```json
+{
+ "status": "failed",
+ "errors": [
+ {
+ "code": "INSUFFICIENT_INVENTORY",
+ "message": "Requested targeting yields no available impressions",
+ "field": "packages[0].targeting",
+ "suggestion": "Expand geographic targeting or increase CPM bid"
+ }
+ ]
+}
+```
+
+
+
+
+### Immediate Success (`completed`)
+
+**Request:**
+```javascript
+const response = await a2a.send({
+ message: {
+ parts: [{
+ kind: 'data',
+ data: {
+ skill: 'create_media_buy',
+ parameters: {
+ buyer_ref: 'summer_campaign_2025',
+ brand_manifest: { name: 'Nike', url: 'https://nike.com' },
+ packages: [
+ {
+ buyer_ref: 'ctv_package',
+ product_id: 'prod_ctv_sports',
+ pricing_option_id: 'cpm_fixed',
+ budget: 50000
+ }
+ ]
+ }
+ }
+ }]
+ }
+});
+```
+
+**Response:**
+```json
+{
+ "status": "completed",
+ "taskId": "task_123",
+ "contextId": "ctx_456",
+ "artifacts": [{
+ "parts": [
+ { "text": "Media buy created successfully" },
+ {
+ "data": {
+ "media_buy_id": "mb_12345",
+ "buyer_ref": "summer_campaign_2025",
+ "creative_deadline": "2025-06-15T23:59:59Z",
+ "packages": [
+ {
+ "package_id": "pkg_001",
+ "buyer_ref": "ctv_package"
+ }
+ ]
+ }
+ }
+ ]
+ }]
}
```
-**Asynchronous (processing)**:
+### Processing (`working`)
+
+Task is actively processing. Use SSE streaming or poll for updates.
+
+**Initial response:**
```json
{
"status": "working",
- "message": "Creating media buy..."
+ "taskId": "task_789",
+ "contextId": "ctx_456"
}
```
-Poll for completion or use webhooks/streaming.
-**Manual Approval Required**:
+**SSE status update:**
+```json
+{
+ "taskId": "task_789",
+ "status": {
+ "state": "working",
+ "message": {
+ "parts": [
+ { "text": "Validating inventory availability..." },
+ {
+ "data": {
+ "percentage": 50,
+ "current_step": "inventory_check"
+ }
+ }
+ ]
+ }
+ }
+}
+```
+
+### Long-Running (`submitted`)
+
+**Request with push notification:**
+```javascript
+const response = await a2a.send({
+ message: {
+ parts: [{
+ kind: 'data',
+ data: {
+ skill: 'create_media_buy',
+ parameters: {
+ buyer_ref: 'enterprise_campaign',
+ packages: [{ budget: 500000 }] // Triggers approval
+ }
+ }
+ }]
+ },
+ pushNotificationConfig: {
+ url: 'https://your-app.com/webhooks/a2a',
+ authentication: {
+ schemes: ['bearer'],
+ credentials: 'your_webhook_secret'
+ }
+ }
+});
+```
+
+**Initial response:**
```json
{
"status": "submitted",
- "message": "Large budget requires sales approval (2-4 hours)"
+ "taskId": "task_abc",
+ "contextId": "ctx_456"
+}
+```
+
+**Webhook POST (Task) when completed:**
+```json
+{
+ "id": "task_abc",
+ "contextId": "ctx_456",
+ "status": {
+ "state": "completed",
+ "message": {
+ "parts": [
+ { "text": "Media buy approved and created" },
+ {
+ "data": {
+ "media_buy_id": "mb_67890",
+ "buyer_ref": "enterprise_campaign",
+ "packages": [{ "package_id": "pkg_002" }]
+ }
+ }
+ ]
+ },
+ "timestamp": "2025-01-22T14:30:00Z"
+ }
+}
+```
+
+### Input Required (`input-required`)
+
+Task is paused waiting for clarification or approval.
+
+**Response:**
+```json
+{
+ "status": "input-required",
+ "taskId": "task_def",
+ "contextId": "ctx_456",
+ "artifacts": [{
+ "parts": [
+ { "text": "The requested budget exceeds your pre-approved limit. Please confirm you want to proceed with $500K spend." },
+ {
+ "data": {
+ "reason": "APPROVAL_REQUIRED",
+ "errors": [
+ {
+ "code": "BUDGET_EXCEEDS_LIMIT",
+ "message": "Requested budget exceeds pre-approved limit",
+ "field": "total_budget"
+ }
+ ]
+ }
+ }
+ ]
+ }]
}
```
-Will take hours to days.
-### Protocol-Specific Handling
+**Follow-up to approve:**
+```javascript
+await a2a.send({
+ contextId: 'ctx_456', // Continue the conversation
+ message: {
+ parts: [{ kind: 'text', text: 'Yes, I confirm the $500K budget' }]
+ }
+});
+```
+
+### Error (`failed`)
-AdCP tasks work across multiple protocols (MCP, A2A, REST). Each protocol handles async operations differently:
+**Response:**
+```json
+{
+ "status": "failed",
+ "taskId": "task_xyz",
+ "artifacts": [{
+ "parts": [
+ { "text": "Failed to create media buy" },
+ {
+ "data": {
+ "errors": [
+ {
+ "code": "INSUFFICIENT_INVENTORY",
+ "message": "Requested targeting yields no available impressions",
+ "suggestion": "Expand geographic targeting"
+ }
+ ]
+ }
+ }
+ ]
+ }]
+}
+```
-- **Status checking**: Polling, webhooks, or streaming
-- **Updates**: Protocol-specific mechanisms
-- **Long-running tasks**: Different timeout and notification patterns
+
+
-See [Task Management](/docs/protocols/task-management) for protocol-specific async patterns and examples.
+For complete async handling patterns, see [Task Management](/docs/protocols/task-management).
## Usage Notes
diff --git a/docs/media-buy/task-reference/get_products.mdx b/docs/media-buy/task-reference/get_products.mdx
index b03196665..665c416f5 100644
--- a/docs/media-buy/task-reference/get_products.mdx
+++ b/docs/media-buy/task-reference/get_products.mdx
@@ -595,6 +595,327 @@ asyncio.run(compare_auth())
See [Authentication Guide](/docs/reference/authentication) for details.
+## Asynchronous Operations
+
+Most product searches complete immediately, but some scenarios require asynchronous processing. When this happens, you'll receive a status other than `completed` and can track progress through webhooks or polling.
+
+### When Search Runs Asynchronously
+
+Product search may require async processing in these situations:
+
+- **Complex searches**: Searching across multiple inventory sources or custom curation
+- **Needs clarification**: Your brief is vague and the system needs more information
+- **Custom products**: Bespoke product packages that require human review
+
+### Async Status Flow
+
+
+
+
+#### Immediate Completion (Most Common)
+
+```json
+POST /api/mcp/call_tool
+
+{
+ "name": "get_products",
+ "arguments": {
+ "brief": "CTV inventory for sports audience",
+ "brand_manifest": { "name": "Nike", "url": "https://nike.com" }
+ }
+}
+
+Response (200 OK):
+{
+ "status": "completed",
+ "message": "Found 3 products matching your requirements",
+ "data": {
+ "products": [...]
+ }
+}
+```
+
+#### Needs Clarification
+
+When the brief is unclear, the system asks for more details:
+
+```json
+Response (200 OK):
+{
+ "status": "input-required",
+ "message": "I need a bit more information. What's your budget range and campaign duration?",
+ "task_id": "task_789",
+ "context_id": "ctx_123",
+ "data": {
+ "reason": "CLARIFICATION_NEEDED",
+ "partial_results": [],
+ "suggestions": ["$50K-$100K", "1 month", "Q1 2024"]
+ }
+}
+```
+
+Continue the conversation with the same `context_id`:
+
+```json
+POST /api/mcp/continue
+
+{
+ "context_id": "ctx_123",
+ "message": "Budget is $75K for a 3-week campaign in March"
+}
+
+Response (200 OK):
+{
+ "status": "completed",
+ "message": "Perfect! Found 5 products within your budget",
+ "data": {
+ "products": [...]
+ }
+}
+```
+
+#### Complex Search (With Webhook)
+
+For searches requiring deep inventory analysis, configure a webhook:
+
+```json
+POST /api/mcp/call_tool
+
+{
+ "name": "get_products",
+ "arguments": {
+ "brief": "Premium inventory across all formats for luxury automotive brand",
+ "brand_manifest": { "name": "Porsche", "url": "https://porsche.com" },
+ "pushNotificationConfig": {
+ "url": "https://buyer.com/webhooks/adcp/get_products",
+ "authentication": {
+ "schemes": ["Bearer"],
+ "credentials": "secret_token_32_chars"
+ }
+ }
+ }
+}
+
+Response (200 OK):
+{
+ "status": "working",
+ "message": "Searching premium inventory across display, video, and audio",
+ "task_id": "task_456",
+ "context_id": "ctx_123",
+ "data": {
+ "percentage": 10,
+ "current_step": "searching_inventory"
+ }
+}
+
+// Later, webhook POST to https://buyer.com/webhooks/adcp/get_products
+{
+ "task_id": "task_456",
+ "task_type": "get_products",
+ "status": "completed",
+ "timestamp": "2025-01-22T10:30:00Z",
+ "message": "Found 12 premium products across all formats",
+ "result": {
+ "products": [...]
+ }
+}
+```
+
+
+
+
+#### Immediate Completion (Most Common)
+
+```json
+POST /api/a2a
+
+{
+ "message": {
+ "role": "user",
+ "parts": [{
+ "kind": "data",
+ "data": {
+ "skill": "get_products",
+ "parameters": {
+ "brief": "CTV inventory for sports audience",
+ "brand_manifest": { "name": "Nike", "url": "https://nike.com" }
+ }
+ }
+ }]
+ }
+}
+
+Response (200 OK):
+{
+ "id": "task_123",
+ "contextId": "ctx_456",
+ "artifact": {
+ "kind": "data",
+ "data": {
+ "products": [...]
+ }
+ },
+ "status": {
+ "state": "completed",
+ "message": {
+ "role": "agent",
+ "parts": [{ "text": "Found 3 products matching your requirements" }]
+ }
+ }
+}
+```
+
+#### Needs Clarification
+
+Real-time updates via SSE when clarification is needed:
+
+```json
+// Initial response
+{
+ "id": "task_789",
+ "contextId": "ctx_123",
+ "status": {
+ "state": "input-required",
+ "message": {
+ "role": "agent",
+ "parts": [
+ { "text": "I need a bit more information. What's your budget range and campaign duration?" },
+ {
+ "data": {
+ "reason": "CLARIFICATION_NEEDED",
+ "suggestions": ["$50K-$100K", "1 month", "Q1 2024"]
+ }
+ }
+ ]
+ }
+ }
+}
+
+// Send follow-up
+POST /api/a2a
+
+{
+ "contextId": "ctx_123",
+ "message": {
+ "role": "user",
+ "parts": [{ "text": "Budget is $75K for a 3-week campaign in March" }]
+ }
+}
+
+// SSE update: task completed
+{
+ "id": "task_789",
+ "contextId": "ctx_123",
+ "artifact": {
+ "kind": "data",
+ "data": { "products": [...] }
+ },
+ "status": {
+ "state": "completed",
+ "message": {
+ "role": "agent",
+ "parts": [{ "text": "Perfect! Found 5 products within your budget" }]
+ }
+ }
+}
+```
+
+#### Complex Search (With Webhook)
+
+Configure push notifications for long searches:
+
+```json
+POST /api/a2a
+
+{
+ "message": {
+ "role": "user",
+ "parts": [{
+ "kind": "data",
+ "data": {
+ "skill": "get_products",
+ "parameters": {
+ "brief": "Premium inventory across all formats for luxury automotive brand",
+ "brand_manifest": { "name": "Porsche", "url": "https://porsche.com" }
+ }
+ }
+ }]
+ },
+ "pushNotificationConfig": {
+ "url": "https://buyer.com/webhooks/a2a/get_products",
+ "authentication": {
+ "schemes": ["bearer"],
+ "credentials": "secret_token_32_chars"
+ }
+ }
+}
+
+Response (200 OK):
+{
+ "id": "task_456",
+ "contextId": "ctx_789",
+ "status": {
+ "state": "working",
+ "message": {
+ "role": "agent",
+ "parts": [
+ { "text": "Searching premium inventory across display, video, and audio" },
+ {
+ "data": {
+ "percentage": 10,
+ "current_step": "searching_inventory"
+ }
+ }
+ ]
+ }
+ }
+}
+
+// Later, webhook POST to https://buyer.com/webhooks/a2a/get_products
+{
+ "id": "task_456",
+ "contextId": "ctx_789",
+ "artifact": {
+ "kind": "data",
+ "data": {
+ "products": [...]
+ }
+ },
+ "status": {
+ "state": "completed",
+ "message": {
+ "role": "agent",
+ "parts": [
+ { "text": "Found 12 premium products across all formats" },
+ {
+ "data": {
+ "products": [...]
+ }
+ }
+ ]
+ },
+ "timestamp": "2025-01-22T10:30:00Z"
+ }
+}
+```
+
+
+
+
+### Status Overview
+
+| Status | When It Happens | What You Do |
+|--------|----------------|-------------|
+| `completed` | Search finished successfully | Process the product results |
+| `input-required` | Need clarification on the brief | Answer the question and continue |
+| `working` | Searching across multiple sources | Wait for webhook or poll for updates |
+| `submitted` | Custom curation queued | Wait for webhook notification |
+| `failed` | Search couldn't complete | Check error message, adjust brief |
+
+**Note:** For the complete status list see [Core Concepts - Task Status System](/docs/protocols/core-concepts#task-status-system).
+
+**Most searches complete immediately.** Async processing is only needed for complex scenarios or when the system needs your input.
+
## Next Steps
After discovering products:
diff --git a/docs/protocols/a2a-guide.mdx b/docs/protocols/a2a-guide.mdx
index 16bdd758f..026e37b55 100644
--- a/docs/protocols/a2a-guide.mdx
+++ b/docs/protocols/a2a-guide.mdx
@@ -214,6 +214,34 @@ return { status: response.status };
**For complete response structure requirements, error handling, and implementation patterns, see [A2A Response Format](/docs/protocols/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.
+
+```javascript
+await a2a.send({
+ message: {
+ parts: [{
+ kind: "data",
+ data: {
+ skill: "create_media_buy",
+ parameters: { /* task params */ }
+ }
+ }]
+ },
+ pushNotificationConfig: {
+ url: "https://buyer.com/webhooks/a2a",
+ 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 [Task Management - Push Notification Integration](/docs/protocols/task-management#push-notification-integration).
+
## SSE Streaming (A2A-Specific)
A2A's key advantage is real-time updates via Server-Sent Events:
@@ -272,8 +300,8 @@ const response = await a2a.send({
}
});
-// Monitor in real-time
-if (response.status === 'working') {
+// Monitor in real-time via SSE
+if (response.status === 'working' || response.status === 'submitted') {
const monitor = new A2aTaskMonitor(response.taskId);
monitor.on('progress', (data) => {
@@ -286,6 +314,191 @@ if (response.status === 'working') {
}
```
+### A2A Webhook Payload Examples
+
+**Example 1: `Task` payload for completed operation**
+
+When a task finishes, the server typically sends the full `Task` object:
+
+```json
+{
+ "id": "task_456",
+ "contextId": "ctx_123",
+ "status": {
+ "state": "completed",
+ "message": {
+ "role": "agent",
+ "parts": [
+ { "text": "Media buy created successfully" },
+ {
+ "data": {
+ "media_buy_id": "mb_12345",
+ "buyer_ref": "nike_q1_campaign",
+ "creative_deadline": "2024-01-30T23:59:59Z",
+ "packages": [
+ { "package_id": "pkg_001", "buyer_ref": "nike_ctv_package" }
+ ]
+ }
+ }
+ ]
+ },
+ "timestamp": "2025-01-22T10:30:00Z"
+ }
+}
+```
+
+**Example 2: `TaskStatusUpdateEvent` for progress updates**
+
+During execution, status changes arrive as lightweight updates:
+
+```json
+{
+ "taskId": "task_456",
+ "contextId": "ctx_123",
+ "status": {
+ "state": "input-required",
+ "message": {
+ "role": "agent",
+ "parts": [
+ { "text": "Campaign budget $150K requires VP approval" },
+ {
+ "data": {
+ "reason": "BUDGET_EXCEEDS_LIMIT"
+ }
+ }
+ ]
+ },
+ "timestamp": "2025-01-22T10:15:00Z"
+ }
+}
+```
+
+The `status.message.parts[].data` payload uses the same AdCP schemas as MCP's `result` field. Schema: [`async-response-data.json`](https://adcontextprotocol.org/schemas/v2/core/async-response-data.json)
+
+### A2A Webhook Payload Types
+
+Per the [A2A specification](https://a2a-protocol.org/latest/specification/#433-push-notification-payload), the server sends different payload types based on the situation:
+
+| Payload Type | When Used | What It Contains |
+|--------------|-----------|------------------|
+| **`Task`** | Final states (`completed`, `failed`, `canceled`) or when full context needed | Complete task object with all history and artifact data |
+| **`TaskStatusUpdateEvent`** | Status transitions during execution (`working`, `input-required`) | Lightweight status change with message parts |
+| **`TaskArtifactUpdateEvent`** | Streaming artifact updates | Artifact data as it becomes available |
+
+For AdCP, most webhooks will be:
+- **`Task`** for final results (`completed`, `failed`)
+- **`TaskStatusUpdateEvent`** for progress updates (`working`, `input-required`)
+
+### 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
+- `completed` → Final result available
+- `failed` → Error details
+- `canceled` → Cancellation confirmed
+
+### Data Schema Validation
+
+The `status.message.parts[].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) |
+| `working` | `[task]-async-response-working.json` | Progress info (`percentage`, `step`) |
+| `input-required` | `[task]-async-response-input-required.json` | Requirements, approval data |
+| `submitted` | `[task]-async-response-submitted.json` | Acknowledgment (usually minimal) |
+
+Schema reference: [`async-response-data.json`](https://adcontextprotocol.org/schemas/v2/core/async-response-data.json)
+
+### Webhook Handler Example
+
+```javascript
+const express = require('express');
+const app = express();
+
+app.post('/webhooks/a2a', async (req, res) => {
+ const webhook = 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' });
+ }
+
+ // Extract data from A2A webhook payload
+ const taskId = webhook.id || webhook.taskId;
+ const contextId = webhook.contextId;
+ const status = webhook.status?.state || webhook.status;
+
+ // Get AdCP data from status.message.parts[].data
+ const dataPart = webhook.status?.message?.parts?.find(p => p.data);
+ const adcpData = dataPart?.data;
+
+ // Handle status changes
+ switch (status) {
+ case 'input-required':
+ // Alert human that input is needed
+ await notifyHuman({
+ task_id: taskId,
+ context_id: contextId,
+ message: webhook.status.message.parts.find(p => p.text)?.text,
+ data: adcpData
+ });
+ break;
+
+ case 'completed':
+ // Process the completed operation
+ if (adcpData?.media_buy_id) {
+ await handleMediaBuyCreated({
+ media_buy_id: adcpData.media_buy_id,
+ buyer_ref: adcpData.buyer_ref,
+ packages: adcpData.packages
+ });
+ }
+ break;
+
+ case 'failed':
+ // Handle failure
+ await handleOperationFailed({
+ task_id: taskId,
+ error: adcpData?.errors,
+ message: webhook.status.message.parts.find(p => p.text)?.text
+ });
+ break;
+
+ case 'working':
+ // Update progress UI
+ await updateProgress({
+ task_id: taskId,
+ percentage: adcpData?.percentage,
+ message: webhook.status.message.parts.find(p => p.text)?.text
+ });
+ 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.
diff --git a/docs/protocols/core-concepts.mdx b/docs/protocols/core-concepts.mdx
index 88b6ec3fd..767214d20 100644
--- a/docs/protocols/core-concepts.mdx
+++ b/docs/protocols/core-concepts.mdx
@@ -173,15 +173,15 @@ Async operations start with `working` and provide updates:
AdCP operations fall into three categories:
1. **Synchronous** - Return immediately with `completed` or `failed`
- - `get_products`, `list_creative_formats`
+ - `list_creative_formats`, `list_authorized_properties`
- Fast operations that don't require external systems
2. **Interactive** - May return `input-required` before proceeding
- - `get_products` (when brief is vague)
- - Operations that need clarification or approval
+ - `get_products` (when brief is vague or needs clarification)
+ - Operations that need user input to proceed
3. **Asynchronous** - Return `working` or `submitted` and require polling/streaming
- - `create_media_buy`, `activate_signal`, `sync_creatives`
+ - `create_media_buy`, `activate_signal`, `sync_creatives`, `get_products`
- Operations that integrate with external systems or require human approval
### Timeout Handling
@@ -273,52 +273,32 @@ All async operations return a `task_id` at the protocol level for tracking:
}
```
-### Protocol-Level Webhook Configuration
+### Push Notification Architecture
-Webhook configuration is handled at the protocol wrapper level, not in individual task parameters:
+Both MCP and A2A use HTTP webhooks for async task updates. Instead of polling, you provide a webhook URL and the server POSTs status changes to you directly.
-#### MCP Webhook Pattern
-```javascript
-class McpAdcpSession {
- async call(tool, params, options = {}) {
- const request = {
- tool: tool,
- arguments: params
- };
+| Aspect | MCP | A2A |
+|--------|-----|-----|
+| **Spec Status** | AdCP specifies this | Native protocol feature |
+| **Configuration** | `pushNotificationConfig` | `pushNotificationConfig` |
+| **Envelope** | `mcp-webhook-payload.json` | `Task` or `TaskStatusUpdateEvent` |
+| **Data Location** | `result` field | `status.message.parts[].data` |
+| **Data Schemas** | **Identical** AdCP schemas | **Identical** AdCP schemas |
- // Protocol-level extensions (like context_id)
- if (this.contextId) {
- request.context_id = this.contextId;
- }
+#### MCP Webhooks
- // Use A2A-compatible push_notification_config
- if (options.push_notification_config) {
- request.push_notification_config = options.push_notification_config;
- }
+MCP doesn't define push notifications. AdCP fills this gap by specifying the webhook configuration (`pushNotificationConfig`) and payload format (`mcp-webhook-payload.json`).
- return await this.mcp.call(request);
- }
-}
+> **Note:** If MCP adds native push notification support in future versions, AdCP will adopt that mechanism in a future major version to maintain alignment with the protocol's evolution.
-// Usage (Bearer token)
-const response = await session.call('create_media_buy',
- { /* task params */ },
- {
- push_notification_config: {
- url: "https://buyer.com/webhooks/adcp",
- authentication: {
- schemes: ["Bearer"],
- credentials: "secret_token_32_chars"
- }
- }
- }
-);
+**Envelope:** [`mcp-webhook-payload.json`](https://adcontextprotocol.org/schemas/v2/core/mcp-webhook-payload.json)
+**Data location:** `result` field
-// Usage (HMAC signature - recommended for production)
+```javascript
const response = await session.call('create_media_buy',
{ /* task params */ },
{
- push_notification_config: {
+ pushNotificationConfig: {
url: "https://buyer.com/webhooks/adcp",
authentication: {
schemes: ["HMAC-SHA256"],
@@ -329,10 +309,13 @@ const response = await session.call('create_media_buy',
);
```
-#### A2A Native Support
+#### A2A Webhooks
+
+A2A defines push notifications natively. Per the [A2A spec](https://a2a-protocol.org/latest/specification/#433-push-notification-payload), the server sends `Task`, `TaskStatusUpdateEvent`, or `TaskArtifactUpdateEvent` depending on what changed.
+
+**Data location:** `status.message.parts[].data`
+
```javascript
-// A2A has native webhook support via PushNotificationConfig
-// AdCP uses the same structure - no mapping needed!
await a2a.send({
message: {
parts: [{
@@ -343,197 +326,140 @@ await a2a.send({
}
}]
},
- push_notification_config: {
- url: "https://buyer.com/webhooks/adcp",
+ pushNotificationConfig: {
+ url: "https://buyer.com/webhooks/a2a",
authentication: {
- schemes: ["HMAC-SHA256"], // or ["Bearer"]
+ schemes: ["bearer"],
credentials: "shared_secret_32_chars"
}
}
});
```
-### Server Decision on Webhook Usage
+#### Unified Data Schemas
-The server decides whether to use webhooks based on the initial response status:
+The **data payload** uses identical AdCP schemas regardless of envelope format:
-- **`completed`, `failed`, `rejected`**: Synchronous response - webhook is NOT called (client already has complete response)
-- **`working`**: Will respond synchronously within ~120 seconds - webhook is NOT called (just wait for the response)
-- **`submitted`**: Long-running async operation - webhook WILL be called on ALL subsequent status changes
-- **Client choice**: Webhook is optional - clients can always poll with `tasks/get`
-
-**Webhook trigger rule:** Webhooks are ONLY used when the initial response status is `submitted`.
+```
+MCP: { result: { media_buy_id, packages, ... } }
+A2A: { status: { message: { parts: [{ data: { media_buy_id, packages, ... } }] } } }
+```
-**When webhooks are called (for `submitted` operations):**
-- Status changes to `input-required` → Webhook called (human needs to respond)
-- Status changes to `completed` → Webhook called (final result)
-- Status changes to `failed` → Webhook called (error details)
-- Status changes to `canceled` → Webhook called (cancellation confirmation)
+Both validate against the same schemas. For `completed`/`failed`, use the full task response schema. For other statuses, use the status-specific schemas.
-### Webhook POST Format
+### When Webhooks Are Called
-When an async operation changes status, the publisher POSTs a payload with protocol fields at the top-level and the task-specific payload nested under `result`.
+Webhooks are triggered when **all** of the following are true:
-#### Webhook Scenarios
+1. **Task type supports async execution** (e.g., `get_products`, `create_media_buy`, `sync_creatives`)
+2. **`pushNotificationConfig` is provided** in the request
+3. **Task requires async processing** — initial response is `working` or `submitted`
-**Scenario 1: Synchronous completion (no webhook)**
-```javascript
-// Initial request
-const response = await session.call('create_media_buy', params, { webhook_url: "..." });
+If the initial response is already terminal (`completed`, `failed`, `rejected`), no webhook is sent — the client already has the final result.
-// Response is immediate and complete - webhook is NOT called
-{
- "status": "completed",
- "media_buy_id": "mb_12345",
- "packages": [...]
-}
-```
+**Status changes that trigger webhooks:**
+- `working` → Progress update
+- `input-required` → Human input needed
+- `completed` → Final result available
+- `failed` → Error details
+- `canceled` → Cancellation confirmed
-**Scenario 2: Quick async processing (no webhook - use working status)**
-```javascript
-// Initial response indicates processing will complete soon
-const response = await session.call('create_media_buy', params, { webhook_url: "..." });
-{
- "status": "working",
- "task_id": "task_789",
- "message": "Creating media buy..."
-}
+### Push Notification Format by Protocol
-// Wait for synchronous response (within ~120 seconds)
-// Webhook is NOT called - client should wait for the response to complete
-// The call will return the final result synchronously
-```
+#### MCP: HTTP Webhook POST
-**Scenario 3: Long-running operation (webhook IS called)**
-```javascript
-// Initial request
-const response = await session.call('create_media_buy', params, {
- webhook_url: "https://buyer.com/webhooks/adcp/create_media_buy/agent_123/op_456"
-});
+When an MCP async operation changes status, the publisher POSTs to your webhook URL.
-// Response indicates long-running async operation
-{
- "status": "submitted",
- "task_id": "task_456",
- "buyer_ref": "nike_q1_campaign_2024",
- "message": "Campaign requires sales approval. Expected time: 2-4 hours."
-}
+**Envelope Schema:** [`mcp-webhook-payload.json`](https://adcontextprotocol.org/schemas/v2/core/mcp-webhook-payload.json)
-// Later: Webhook POST when approval is needed
+```http
POST /webhooks/adcp/create_media_buy/agent_123/op_456 HTTP/1.1
-{
- "operation_id": "op_456",
- "task_id": "task_456",
- "task_type": "create_media_buy",
- "status": "input-required",
- "timestamp": "2025-01-22T10:15:00Z",
- "message": "Please approve $150K campaign to proceed",
- "result": {
- "buyer_ref": "nike_q1_campaign_2024"
- }
-}
+Host: buyer.example.com
+Authorization: Bearer your-secret-token
+Content-Type: application/json
-// Later: Webhook POST when approved and completed (result nested)
-POST /webhooks/adcp/create_media_buy/agent_123/op_456 HTTP/1.1
{
- "operation_id": "op_456",
"task_id": "task_456",
"task_type": "create_media_buy",
"status": "completed",
"timestamp": "2025-01-22T10:30:00Z",
+ "message": "Media buy created successfully",
"result": {
"media_buy_id": "mb_12345",
"buyer_ref": "nike_q1_campaign_2024",
"creative_deadline": "2024-01-30T23:59:59Z",
"packages": [
- {
- "package_id": "pkg_12345_001",
- "buyer_ref": "nike_ctv_sports_package"
- },
- {
- "package_id": "pkg_12345_002",
- "buyer_ref": "nike_audio_drive_package"
- }
+ { "package_id": "pkg_12345_001", "buyer_ref": "nike_ctv_sports_package" }
]
}
}
```
-#### For Other Async Operations
+#### A2A Webhook POST
-Each async operation posts its specific response schema:
+A2A sends `Task` (for final states) or `TaskStatusUpdateEvent` (for progress updates):
-- **`activate_signal`** → `activate-signal-response.json`
-- **`sync_creatives`** → `sync-creatives-response.json`
-- **`update_media_buy`** → `update-media-buy-response.json`
+```json
+{
+ "id": "task_456",
+ "contextId": "ctx_123",
+ "status": {
+ "state": "completed",
+ "message": {
+ "role": "agent",
+ "parts": [
+ { "text": "Media buy created successfully" },
+ {
+ "data": {
+ "media_buy_id": "mb_12345",
+ "buyer_ref": "nike_q1_campaign_2024",
+ "creative_deadline": "2024-01-30T23:59:59Z",
+ "packages": [
+ { "package_id": "pkg_12345_001", "buyer_ref": "nike_ctv_sports_package" }
+ ]
+ }
+ }
+ ]
+ },
+ "timestamp": "2025-01-22T10:30:00Z"
+ }
+}
+```
-#### Webhook URL Patterns
+#### Status-Specific Data Schemas
-Structure your webhook URLs to identify the operation and agent:
+The data payload (`result` in MCP, `status.message.parts[].data` in A2A) uses status-specific schemas:
-```
-https://buyer.com/webhooks/adcp/{task_name}/{agent_id}/{operation_id}
-```
+| Status | Data Schema | Contents |
+|--------|-------------|----------|
+| `completed` | `[task]-response.json` | Full task response (success branch) |
+| `failed` | `[task]-response.json` | Full task response (error branch) |
+| `working` | `[task]-async-response-working.json` | Progress info (`percentage`, `step`) |
+| `input-required` | `[task]-async-response-input-required.json` | Requirements, approval data |
+| `submitted` | `[task]-async-response-submitted.json` | Acknowledgment (usually minimal) |
-**Example URLs:**
-- `https://buyer.com/webhooks/adcp/create_media_buy/agent_abc/op_xyz`
-- `https://buyer.com/webhooks/adcp/activate_signal/agent_abc/op_123`
-- `https://buyer.com/webhooks/adcp/sync_creatives/agent_abc/op_456`
+#### Supported Async Operations
-Your webhook handler can parse the URL path to route to the correct handler based on the task name.
+Each async operation uses its main response schema for `completed`/`failed` statuses:
-#### Webhook Payload Structure
+| Task | Response Schema |
+|------|-----------------|
+| `get_products` | `get-products-response.json` |
+| `create_media_buy` | `create-media-buy-response.json` |
+| `update_media_buy` | `update-media-buy-response.json` |
+| `sync_creatives` | `sync-creatives-response.json` |
-Every webhook POST contains protocol fields plus a `result` object for the task-specific payload of that status.
+#### MCP Webhook URL Patterns
-**`input-required` webhook (human needs to respond):**
-```json
-{
- "operation_id": "op_456",
- "task_id": "task_456",
- "task_type": "create_media_buy",
- "status": "input-required",
- "message": "Campaign budget requires VP approval to proceed",
- "result": {
- "buyer_ref": "nike_q1_campaign_2024"
- }
-}
-```
+For MCP HTTP webhooks, structure URLs to identify the operation:
-**`completed` webhook (operation finished - full create_media_buy response):**
-```json
-{
- "operation_id": "op_456",
- "task_id": "task_456",
- "task_type": "create_media_buy",
- "status": "completed",
- "result": {
- "media_buy_id": "mb_12345",
- "buyer_ref": "nike_q1_campaign_2024",
- "creative_deadline": "2024-01-30T23:59:59Z",
- "packages": [
- {
- "package_id": "pkg_001",
- "buyer_ref": "nike_ctv_package"
- }
- ]
- }
-}
```
-
-**`failed` webhook (operation failed):**
-```json
-{
- "operation_id": "op_456",
- "task_id": "task_456",
- "task_type": "create_media_buy",
- "status": "failed",
- "message": "Requested targeting yielded 0 available impressions",
- "error": "insufficient_inventory"
-}
+https://buyer.com/webhooks/adcp/{task_name}/{agent_id}/{operation_id}
```
-**Key principle:** Webhooks are ONLY called for `submitted` operations, and each webhook contains an envelope plus `result` matching the task's response schema.
+Your webhook handler can parse the URL path to route to the correct handler.
+
+**Key principle:** For async tasks with `pushNotificationConfig`, push notifications are sent for all status changes after the initial response. The data payload uses the same schema regardless of transport (MCP webhook or A2A native message).
### Task State Reconciliation
diff --git a/docs/protocols/mcp-guide.mdx b/docs/protocols/mcp-guide.mdx
index b4f615e16..4ce235e52 100644
--- a/docs/protocols/mcp-guide.mdx
+++ b/docs/protocols/mcp-guide.mdx
@@ -226,8 +226,13 @@ const refined = await session.call('get_products', {
```
#### Async Operations with Webhooks
+
+MCP doesn't define push notifications. AdCP fills this gap by specifying the webhook configuration (`pushNotificationConfig`) and payload format (`mcp-webhook-payload.json`). When you configure a webhook, the server will POST task updates to your URL instead of requiring you to poll.
+
+**Webhook Envelope:** [`mcp-webhook-payload.json`](https://adcontextprotocol.org/schemas/v2/core/mcp-webhook-payload.json)
+
```javascript
-// Create media buy with webhook configuration
+// Configure webhook when calling MCP tool
const response = await session.call('create_media_buy',
{
buyer_ref: "nike_q1_2025",
@@ -235,10 +240,10 @@ const response = await session.call('create_media_buy',
budget: { total: 150000, currency: "USD" }
},
{
- push_notification_config: {
+ pushNotificationConfig: {
url: "https://buyer.com/webhooks/adcp",
authentication: {
- schemes: ["HMAC-SHA256"], // or ["Bearer"] for simple auth
+ schemes: ["HMAC-SHA256"], // or ["bearer"] for simple auth
credentials: "shared_secret_32_chars"
}
}
@@ -247,12 +252,155 @@ const response = await session.call('create_media_buy',
if (response.status === 'submitted') {
console.log(`Task ${response.task_id} submitted for long-running execution`);
- // Webhook will notify when complete, or poll manually
+ // Server will POST status updates to your webhook URL
} else if (response.status === 'completed') {
console.log(`Media buy created: ${response.media_buy_id}`);
}
```
+**Webhook POST format:**
+```json
+{
+ "task_id": "task_456",
+ "task_type": "create_media_buy",
+ "status": "completed",
+ "timestamp": "2025-01-22T10:30:00Z",
+ "result": {
+ "media_buy_id": "mb_12345",
+ "buyer_ref": "nike_q1_2025",
+ "packages": [...]
+ }
+}
+```
+
+The `result` field contains the AdCP data payload. For `completed`/`failed` statuses, this is the full task response (e.g., `create-media-buy-response.json`). For other statuses, use the status-specific schemas (e.g., `create-media-buy-async-response-working.json`).
+
+#### MCP Webhook Envelope Fields
+
+The [`mcp-webhook-payload.json`](https://adcontextprotocol.org/schemas/v2/core/mcp-webhook-payload.json) envelope includes:
+
+**Required fields:**
+- `task_id` — Unique task identifier for correlation
+- `task_type` — Task name (e.g., "create_media_buy", "sync_creatives")
+- `status` — Current task status (completed, failed, working, input-required, etc.)
+- `timestamp` — ISO 8601 timestamp when webhook was generated
+
+**Optional fields:**
+- `operation_id` — Correlates a sequence of updates for the same operation
+- `domain` — AdCP domain ("media-buy" or "signals")
+- `context_id` — Conversation/session identifier
+- `message` — Human-readable context about the status change
+
+**Data field:**
+- `result` — Task-specific AdCP payload (see Data Schema Validation below)
+
+#### 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
+- `completed` → Final result available
+- `failed` → Error details
+
+#### Data Schema Validation
+
+The `result` field in MCP 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) |
+| `working` | `[task]-async-response-working.json` | Progress info (`percentage`, `step`) |
+| `input-required` | `[task]-async-response-input-required.json` | Requirements, approval data |
+| `submitted` | `[task]-async-response-submitted.json` | Acknowledgment (usually minimal) |
+
+Schema reference: [`async-response-data.json`](https://adcontextprotocol.org/schemas/v2/core/async-response-data.json)
+
+#### Webhook Handler Example
+
+```javascript
+const express = require('express');
+const app = express();
+
+app.post('/webhooks/adcp/:task_type/:agent_id/:operation_id', async (req, res) => {
+ const { task_type, agent_id, operation_id } = req.params;
+ const webhook = req.body;
+
+ // Verify webhook authenticity (HMAC-SHA256 example)
+ const signature = req.headers['x-adcp-signature'];
+ const timestamp = req.headers['x-adcp-timestamp'];
+ if (!verifySignature(webhook, signature, timestamp)) {
+ return res.status(401).json({ error: 'Invalid signature' });
+ }
+
+ // Handle status changes
+ switch (webhook.status) {
+ case 'input-required':
+ // Alert human that input is needed
+ await notifyHuman({
+ operation_id,
+ message: webhook.message,
+ context_id: webhook.context_id,
+ data: webhook.result
+ });
+ break;
+
+ case 'completed':
+ // Process the completed operation
+ if (task_type === 'create_media_buy') {
+ await handleMediaBuyCreated({
+ media_buy_id: webhook.result.media_buy_id,
+ buyer_ref: webhook.result.buyer_ref,
+ packages: webhook.result.packages
+ });
+ }
+ break;
+
+ case 'failed':
+ // Handle failure
+ await handleOperationFailed({
+ operation_id,
+ error: webhook.result?.errors,
+ message: webhook.message
+ });
+ break;
+
+ case 'working':
+ // Update progress UI
+ await updateProgress({
+ operation_id,
+ percentage: webhook.result?.percentage,
+ message: webhook.message
+ });
+ break;
+
+ case 'canceled':
+ await handleOperationCanceled(operation_id, webhook.message);
+ break;
+ }
+
+ // Always return 200 for successful processing
+ res.status(200).json({ status: 'processed' });
+});
+
+function verifySignature(payload, signature, timestamp) {
+ const crypto = require('crypto');
+ const expectedSig = crypto
+ .createHmac('sha256', process.env.WEBHOOK_SECRET)
+ .update(timestamp + JSON.stringify(payload))
+ .digest('hex');
+ return signature === `sha256=${expectedSig}`;
+}
+```
+
#### Task Management and Polling
```javascript
// Check status of specific task
@@ -292,11 +440,18 @@ async function handleContextExpiration(session, tool, params) {
**Key Difference**: Unlike A2A which manages context automatically, MCP requires explicit context_id management.
-## Async Operations (MCP-Specific)
+## Handling Async Operations
+
+When a task returns `working` or `submitted` status, you have two options for receiving updates:
-MCP handles long-running operations through polling with `context_id`:
+| Approach | Best For | Trade-offs |
+|----------|----------|------------|
+| **Polling** | Simple integrations, short tasks | Easy to implement, but inefficient for long waits |
+| **Webhooks** | Production systems, long-running tasks | More efficient, but requires a public endpoint |
-### Polling Pattern
+### Option 1: Polling
+
+Use `tasks/get` to check task status periodically:
```javascript
async function waitForCompletion(session, initialResponse) {
@@ -304,10 +459,11 @@ async function waitForCompletion(session, initialResponse) {
return initialResponse; // Already completed
}
+ // Poll more frequently for 'working' (will finish soon)
+ // Poll less frequently for 'submitted' (may take hours)
let pollInterval = initialResponse.status === 'working' ? 5000 : 30000;
while (true) {
- // Poll using tasks/get with task_id
const response = await session.pollTask(initialResponse.task_id, true);
if (['completed', 'failed', 'canceled'].includes(response.status)) {
@@ -315,26 +471,50 @@ async function waitForCompletion(session, initialResponse) {
}
if (response.status === 'input-required') {
- // Handle user input requirement
const input = await promptUser(response.message);
- // Continue conversation with context_id
return session.call('create_media_buy', {
context_id: response.context_id,
additional_info: input
});
}
- // Adjust polling frequency based on status
pollInterval = response.status === 'working' ? 5000 : 30000;
await new Promise(resolve => setTimeout(resolve, pollInterval));
}
}
```
-### Async Operation Example
+### Option 2: Webhooks
+
+Configure a webhook URL and the server will POST updates to you directly. This is more efficient for long-running tasks since you don't need to keep polling.
+
+```javascript
+const response = await session.call('create_media_buy',
+ {
+ buyer_ref: "nike_q1_2025",
+ packages: [...],
+ budget: { total: 150000, currency: "USD" }
+ },
+ {
+ pushNotificationConfig: {
+ url: "https://buyer.com/webhooks/adcp",
+ authentication: {
+ schemes: ["HMAC-SHA256"],
+ credentials: "shared_secret_32_chars"
+ }
+ }
+ }
+);
+
+// If status is 'submitted', the server will POST updates to your webhook
+// You don't need to poll - just wait for the webhook
+```
+
+See [Task Management](/docs/protocols/task-management) for webhook payload formats and handling examples.
+
+### Handling Different Statuses
```javascript
-// Start async operation
const initial = await session.call('create_media_buy', {
buyer_ref: "nike_q1_2025",
packages: [...],
@@ -343,30 +523,30 @@ const initial = await session.call('create_media_buy', {
switch (initial.status) {
case 'completed':
- console.log('Created immediately:', initial.media_buy_id);
+ // Done immediately - no async handling needed
+ console.log('Created:', initial.media_buy_id);
break;
case 'working':
- console.log('Processing, will complete within 2 minutes...');
+ // Will finish within ~2 minutes - poll or wait
+ console.log('Processing...');
const final = await waitForCompletion(session, initial);
console.log('Created:', final.result.media_buy_id);
break;
case 'submitted':
- console.log(`Queued for approval - long-running operation`);
- console.log(`Track with task ID: ${initial.task_id}`);
- // Use webhook or poll manually
+ // Long-running (hours/days) - use webhooks or poll infrequently
+ console.log(`Task ${initial.task_id} queued for approval`);
+ // Webhook will notify when complete, or poll manually
break;
case 'input-required':
- console.log('Need additional info:', initial.message);
- // Handle user input
+ // Blocked on user input
+ console.log('Need more info:', initial.message);
break;
}
```
-**Note**: Use `tasks/get` for polling specific tasks, or `tasks/list` for state reconciliation. See [Task Management](/docs/protocols/task-management) for complete documentation on task tracking patterns and webhook integration.
-
## Integration Example
```javascript
diff --git a/docs/protocols/protocol-comparison.mdx b/docs/protocols/protocol-comparison.mdx
index 6889f223c..dd2fe254a 100644
--- a/docs/protocols/protocol-comparison.mdx
+++ b/docs/protocols/protocol-comparison.mdx
@@ -154,45 +154,27 @@ await a2a.send({
});
```
-## Webhook & Task Management Differences
+## Push Notification Architecture
-### Webhook Configuration
+Both MCP and A2A use HTTP webhooks for async task updates. AdCP keeps the **envelope format** protocol-specific while using **identical data schemas** for the business payload.
-Both protocols support webhooks but with different implementation approaches:
-
-#### MCP: Protocol Wrapper Extension
-```javascript
-// AdCP uses A2A-compatible structure for MCP as well
-class McpAdcpSession {
- async call(tool, params, options = {}) {
- const request = { tool, arguments: params };
-
- // Same structure as A2A - no mapping needed
- if (options.push_notification_config) {
- request.push_notification_config = options.push_notification_config;
- }
+| Aspect | MCP | A2A |
+|--------|-----|-----|
+| **Spec Status** | AdCP specifies this | Native protocol feature |
+| **Configuration** | `pushNotificationConfig` | `pushNotificationConfig` |
+| **Envelope** | `mcp-webhook-payload.json` | `Task`, `TaskStatusUpdateEvent`, or `TaskArtifactUpdateEvent` |
+| **Data Location** | `result` field | `status.message.parts[].data` |
+| **Data Schemas** | **Identical** AdCP schemas | **Identical** AdCP schemas |
- return await this.mcp.call(request);
- }
-}
-```
+### Key Principles
-#### A2A: Native Push Notifications
-```javascript
-// Built-in PushNotificationConfig - AdCP uses this structure universally
-await a2a.send({
- message: { /* task */ },
- push_notification_config: {
- url: "https://buyer.com/webhooks",
- authentication: {
- schemes: ["HMAC-SHA256"], // or ["Bearer"]
- credentials: "shared_secret_32_chars"
- }
- }
-});
-```
+- **Same data, different envelopes**: The AdCP payload (media buy data, product lists, etc.) is identical regardless of protocol
+- **Protocol-native delivery**: MCP uses `mcp-webhook-payload.json`, A2A uses native `Task` objects
+- **Unified configuration**: Both use `pushNotificationConfig` with the same authentication structure
-**Key Insight:** AdCP adopts A2A's `PushNotificationConfig` structure as the universal webhook configuration format across all protocols. This eliminates protocol-specific mapping and provides a consistent developer experience.
+**For protocol-specific examples and implementation details:**
+- MCP webhooks: See [MCP Guide - Async Operations](/docs/protocols/mcp-guide#async-operations-mcp-specific)
+- A2A webhooks: See [A2A Guide - Push Notifications](/docs/protocols/a2a-guide#push-notifications-a2a-specific)
### Task Management
diff --git a/docs/protocols/task-management.mdx b/docs/protocols/task-management.mdx
index fc156cb15..384bc64cf 100644
--- a/docs/protocols/task-management.mdx
+++ b/docs/protocols/task-management.mdx
@@ -1,6 +1,6 @@
---
title: Task Management
-sidebar_position: 8
+sidebar_position: 6
---
# Task Management
@@ -440,169 +440,10 @@ await a2a.send({
## Webhook Integration
-Task management integrates with protocol-level webhook configuration for push notifications.
+For async operations, you can configure webhooks instead of polling. See [Protocol Comparison - Push Notifications](/docs/protocols/protocol-comparison#push-notification-architecture) for protocol differences and [Core Concepts - Webhook Reliability](/docs/protocols/core-concepts#webhook-reliability) for implementation patterns.
-### Webhook Configuration
-
-Configure webhooks at the protocol level when making async task calls. See **[Core Concepts: Protocol-Level Webhook Configuration](/docs/protocols/core-concepts.mdx#protocol-level-webhook-configuration)** for complete setup examples.
-
-**Quick example:**
-```javascript
-const response = await session.call('create_media_buy',
- { /* task params */ },
- {
- push_notification_config: {
- url: "https://buyer.com/webhooks/adcp/create_media_buy/agent_id/operation_id",
- authentication: {
- schemes: ["HMAC-SHA256"], // or ["Bearer"] for simple auth
- credentials: "shared_secret_32_chars"
- }
- }
- }
-);
-```
-
-### Webhook POST Format
-
-When a task's status changes, the publisher POSTs a payload with protocol fields at the top-level and the task-specific payload nested under `result` to your webhook URL.
-
-**Webhook Payload Schema**: [`https://adcontextprotocol.org/schemas/v2/core/webhook-payload.json`](https://adcontextprotocol.org/schemas/v2/core/webhook-payload.json)
-
-**Top-level fields:**
-- `operation_id` (required) — Correlates a sequence of updates for this operation
-- `domain` - AdCP domain ("media-buy" or "signals")
-- `task_type` (required) — e.g., "create_media_buy", "sync_creatives", "activate_signal"
-- `status` (required) — Current task status
-- `task_id` (optional) — Present when server exposes task tracking id
-- `context_id` (optional) — Conversation/session id
-- `message` (optional) — Human-readable context
-- `timestamp` (optional) — ISO 8601 time when webhook was generated
-- `result` (optional) — Task-specific payload for this status
-- `error` (optional) — Error message string when status is `failed`
-
-**Webhook trigger rule:** Webhooks are ONLY used when the initial response status is `submitted` (long-running operations).
-
-**When webhooks are NOT triggered:**
-- Initial response is `completed`, `failed`, or `rejected` → Synchronous response, client already has result
-- Initial response is `working` → Will complete synchronously within ~120 seconds, client should wait for response
-
-**When webhooks ARE triggered (for `submitted` operations only):**
-- Status changes to `input-required` → Webhook called (alerts that human input needed)
-- Status changes to `completed` → Webhook called (final result available)
-- Status changes to `failed` → Webhook called (error details provided)
-- Status changes to `canceled` → Webhook called (cancellation confirmed)
-
-**Example: `input-required` webhook (human approval needed):**
-```http
-POST /webhooks/adcp/create_media_buy/agent_123/op_456 HTTP/1.1
-Host: buyer.example.com
-Authorization: Bearer your-secret-token
-Content-Type: application/json
-
-{
- "operation_id": "op_456",
- "task_id": "task_456",
- "task_type": "create_media_buy",
- "domain": "media-buy",
- "status": "input-required",
- "timestamp": "2025-01-22T10:15:00Z",
- "context_id": "ctx_abc123",
- "message": "Campaign budget $150K requires VP approval to proceed",
- "result": {
- "buyer_ref": "nike_q1_campaign_2024"
- }
-}
-```
-
-**Example: `completed` webhook (after approval granted - full create_media_buy response):**
-```http
-POST /webhooks/adcp/create_media_buy/agent_123/op_456 HTTP/1.1
-Host: buyer.example.com
-Authorization: Bearer your-secret-token
-Content-Type: application/json
-
-{
- "operation_id": "op_456",
- "task_id": "task_456",
- "task_type": "create_media_buy",
- "domain": "media-buy",
- "status": "completed",
- "timestamp": "2025-01-22T10:30:00Z",
- "message": "Media buy created successfully with 2 packages ready for creative assignment",
- "result": {
- "media_buy_id": "mb_12345",
- "buyer_ref": "nike_q1_campaign_2024",
- "creative_deadline": "2024-01-30T23:59:59Z",
- "packages": [
- { "package_id": "pkg_12345_001", "buyer_ref": "nike_ctv_package" }
- ]
- }
-}
-```
-
-The webhook receives the **full response object** for each status, not just a notification. This means your webhook handler gets all the context and data needed to take appropriate action.
-
-### Webhook Handling Example
-
-```javascript
-app.post('/webhooks/adcp/:task_type/:agent_id/:operation_id', async (req, res) => {
- const { task_type, agent_id, operation_id } = req.params;
- const response = req.body;
-
- // Webhooks are only called for 'submitted' operations
- // So we only need to handle status changes that occur after submission
- switch (response.status) {
- case 'input-required':
- // Alert human that input is needed
- await notifyHuman({
- operation_id,
- message: response.message,
- context_id: response.context_id,
- approval_data: response.data
- });
- break;
-
- case 'completed':
- // Process the completed operation
- if (task_type === 'create_media_buy') {
- await handleMediaBuyCreated({
- media_buy_id: response.result?.media_buy_id,
- buyer_ref: response.result?.buyer_ref,
- packages: response.result?.packages,
- creative_deadline: response.result?.creative_deadline
- });
- }
- break;
-
- case 'failed':
- // Handle failure
- await handleOperationFailed({
- operation_id,
- error: response.error,
- message: response.message
- });
- break;
-
- case 'canceled':
- // Handle cancellation
- await handleOperationCanceled(operation_id, response.message);
- break;
- }
-
- res.status(200).json({ status: 'processed' });
-});
-```
-
-### Webhook Reliability
-
-**Important**: Webhooks use at-least-once delivery semantics and may be duplicated or arrive out of order.
-
-See **[Core Concepts: Webhook Reliability](/docs/protocols/core-concepts.mdx#webhook-reliability)** for detailed implementation guidance including:
-- Idempotent webhook handlers
-- Sequence handling and out-of-order detection
-- Security considerations (signature verification)
-- Polling as backup mechanism
-- Replay attack prevention
+**Webhook trigger conditions:**
+Webhooks fire when the task type supports async, `pushNotificationConfig` is provided, and the task actually runs asynchronously (initial response is `working` or `submitted`).
## Error Handling
diff --git a/static/schemas/source/core/async-response-data.json b/static/schemas/source/core/async-response-data.json
new file mode 100644
index 000000000..564db8145
--- /dev/null
+++ b/static/schemas/source/core/async-response-data.json
@@ -0,0 +1,89 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/core/async-response-data.json",
+ "title": "AdCP Async Response Data",
+ "description": "Union of all possible data payloads for async task webhook responses. For completed/failed statuses, use the main task response schema. For working/input-required/submitted, use the status-specific schemas.",
+ "anyOf": [
+ {
+ "title": "GetProductsResponse",
+ "description": "Response for completed or failed get_products",
+ "$ref": "/schemas/media-buy/get-products-response.json"
+ },
+ {
+ "title": "GetProductsAsyncWorking",
+ "description": "Progress data for working get_products",
+ "$ref": "/schemas/media-buy/get-products-async-response-working.json"
+ },
+ {
+ "title": "GetProductsAsyncInputRequired",
+ "description": "Input requirements for get_products needing clarification",
+ "$ref": "/schemas/media-buy/get-products-async-response-input-required.json"
+ },
+ {
+ "title": "GetProductsAsyncSubmitted",
+ "description": "Acknowledgment for submitted get_products (custom curation)",
+ "$ref": "/schemas/media-buy/get-products-async-response-submitted.json"
+ },
+ {
+ "title": "CreateMediaBuyResponse",
+ "description": "Response for completed or failed create_media_buy",
+ "$ref": "/schemas/media-buy/create-media-buy-response.json"
+ },
+ {
+ "title": "CreateMediaBuyAsyncWorking",
+ "description": "Progress data for working create_media_buy",
+ "$ref": "/schemas/media-buy/create-media-buy-async-response-working.json"
+ },
+ {
+ "title": "CreateMediaBuyAsyncInputRequired",
+ "description": "Input requirements for create_media_buy needing user input",
+ "$ref": "/schemas/media-buy/create-media-buy-async-response-input-required.json"
+ },
+ {
+ "title": "CreateMediaBuyAsyncSubmitted",
+ "description": "Acknowledgment for submitted create_media_buy",
+ "$ref": "/schemas/media-buy/create-media-buy-async-response-submitted.json"
+ },
+ {
+ "title": "UpdateMediaBuyResponse",
+ "description": "Response for completed or failed update_media_buy",
+ "$ref": "/schemas/media-buy/update-media-buy-response.json"
+ },
+ {
+ "title": "UpdateMediaBuyAsyncWorking",
+ "description": "Progress data for working update_media_buy",
+ "$ref": "/schemas/media-buy/update-media-buy-async-response-working.json"
+ },
+ {
+ "title": "UpdateMediaBuyAsyncInputRequired",
+ "description": "Input requirements for update_media_buy needing user input",
+ "$ref": "/schemas/media-buy/update-media-buy-async-response-input-required.json"
+ },
+ {
+ "title": "UpdateMediaBuyAsyncSubmitted",
+ "description": "Acknowledgment for submitted update_media_buy",
+ "$ref": "/schemas/media-buy/update-media-buy-async-response-submitted.json"
+ },
+ {
+ "title": "SyncCreativesResponse",
+ "description": "Response for completed or failed sync_creatives",
+ "$ref": "/schemas/media-buy/sync-creatives-response.json"
+ },
+ {
+ "title": "SyncCreativesAsyncWorking",
+ "description": "Progress data for working sync_creatives",
+ "$ref": "/schemas/media-buy/sync-creatives-async-response-working.json"
+ },
+ {
+ "title": "SyncCreativesAsyncInputRequired",
+ "description": "Input requirements for sync_creatives needing user input",
+ "$ref": "/schemas/media-buy/sync-creatives-async-response-input-required.json"
+ },
+ {
+ "title": "SyncCreativesAsyncSubmitted",
+ "description": "Acknowledgment for submitted sync_creatives",
+ "$ref": "/schemas/media-buy/sync-creatives-async-response-submitted.json"
+ }
+ ]
+}
+
diff --git a/static/schemas/source/core/mcp-webhook-payload.json b/static/schemas/source/core/mcp-webhook-payload.json
new file mode 100644
index 000000000..054bb4eb6
--- /dev/null
+++ b/static/schemas/source/core/mcp-webhook-payload.json
@@ -0,0 +1,147 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/core/mcp-webhook-payload.json",
+ "title": "MCP Webhook Payload",
+ "description": "Standard envelope for HTTP-based push notifications (MCP). This defines the wire format sent to the URL configured in `pushNotificationConfig`. NOTE: This envelope is NOT used in A2A integration, which uses native Task/TaskStatusUpdateEvent messages with the AdCP payload nested in `status.message.parts[].data`.",
+ "type": "object",
+ "properties": {
+ "operation_id": {
+ "type": "string",
+ "description": "Publisher-defined operation identifier correlating a sequence of task updates across webhooks."
+ },
+ "task_id": {
+ "type": "string",
+ "description": "Unique identifier for this task. Use this to correlate webhook notifications with the original task submission."
+ },
+ "task_type": {
+ "$ref": "/schemas/enums/task-type.json",
+ "description": "Type of AdCP operation that triggered this webhook. Enables webhook handlers to route to appropriate processing logic."
+ },
+ "domain": {
+ "$ref": "/schemas/enums/adcp-domain.json",
+ "description": "AdCP domain this task belongs to. Helps classify the operation type at a high level."
+ },
+ "status": {
+ "$ref": "/schemas/enums/task-status.json",
+ "description": "Current task status. Webhooks are triggered for status changes after initial submission."
+ },
+ "timestamp": {
+ "type": "string",
+ "format": "date-time",
+ "description": "ISO 8601 timestamp when this webhook was generated."
+ },
+ "message": {
+ "type": "string",
+ "description": "Human-readable summary of the current task state. Provides context about what happened and what action may be needed."
+ },
+ "context_id": {
+ "type": "string",
+ "description": "Session/conversation identifier. Use this to continue the conversation if input-required status needs clarification or additional parameters."
+ },
+ "result": {
+ "$ref": "/schemas/core/async-response-data.json",
+ "description": "Task-specific payload matching the status. For completed/failed, contains the full task response. For working/input-required/submitted, contains status-specific data. This is the data layer that AdCP specs - same structure used in A2A status.message.parts[].data."
+ }
+ },
+ "required": ["task_id", "task_type", "status", "timestamp"],
+ "additionalProperties": true,
+ "examples": [
+ {
+ "description": "Webhook for input-required status (human approval needed)",
+ "data": {
+ "operation_id": "op_456",
+ "task_id": "task_456",
+ "task_type": "create_media_buy",
+ "domain": "media-buy",
+ "status": "input-required",
+ "timestamp": "2025-01-22T10:15:00Z",
+ "context_id": "ctx_abc123",
+ "message": "Campaign budget $150K requires VP approval to proceed",
+ "result": {
+ "reason": "BUDGET_EXCEEDS_LIMIT",
+ "errors": [
+ {
+ "code": "APPROVAL_REQUIRED",
+ "message": "Budget exceeds auto-approval threshold",
+ "field": "total_budget"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "description": "Webhook for completed create_media_buy",
+ "data": {
+ "operation_id": "op_456",
+ "task_id": "task_456",
+ "task_type": "create_media_buy",
+ "domain": "media-buy",
+ "status": "completed",
+ "timestamp": "2025-01-22T10:30:00Z",
+ "message": "Media buy created successfully with 2 packages ready for creative assignment",
+ "result": {
+ "media_buy_id": "mb_12345",
+ "buyer_ref": "nike_q1_campaign_2024",
+ "creative_deadline": "2024-01-30T23:59:59Z",
+ "packages": [
+ {
+ "package_id": "pkg_12345_001",
+ "buyer_ref": "nike_ctv_package",
+ "product_id": "ctv_sports_premium",
+ "budget": 60000,
+ "pacing": "even",
+ "pricing_option_id": "cpm-fixed-sports",
+ "paused": false,
+ "creative_assignments": [],
+ "format_ids_to_provide": [
+ {
+ "agent_url": "https://creative.adcontextprotocol.org",
+ "id": "video_standard_30s"
+ }
+ ]
+ }
+ ]
+ }
+ }
+ },
+ {
+ "description": "Webhook for working status with progress",
+ "data": {
+ "operation_id": "op_456",
+ "task_id": "task_456",
+ "task_type": "create_media_buy",
+ "domain": "media-buy",
+ "status": "working",
+ "timestamp": "2025-01-22T10:20:00Z",
+ "message": "Validating inventory availability...",
+ "result": {
+ "percentage": 50,
+ "current_step": "inventory_validation",
+ "step_number": 2,
+ "total_steps": 4
+ }
+ }
+ },
+ {
+ "description": "Webhook for failed sync_creatives",
+ "data": {
+ "operation_id": "op_789",
+ "task_id": "task_789",
+ "task_type": "sync_creatives",
+ "domain": "media-buy",
+ "status": "failed",
+ "timestamp": "2025-01-22T10:46:00Z",
+ "message": "Creative sync failed due to invalid asset URLs",
+ "result": {
+ "errors": [
+ {
+ "code": "INVALID_ASSET_URL",
+ "message": "One or more creative assets could not be accessed",
+ "field": "creatives[0].asset_url"
+ }
+ ]
+ }
+ }
+ }
+ ]
+}
diff --git a/static/schemas/source/core/webhook-payload.json b/static/schemas/source/core/webhook-payload.json
deleted file mode 100644
index 35b824b35..000000000
--- a/static/schemas/source/core/webhook-payload.json
+++ /dev/null
@@ -1,231 +0,0 @@
-{
- "$schema": "http://json-schema.org/draft-07/schema#",
- "$id": "/schemas/core/webhook-payload.json",
- "title": "Webhook Payload",
- "description": "Payload structure sent to webhook endpoints when async task status changes. Protocol-level fields are at the top level and the task-specific payload is nested under the 'result' field. This schema represents what your webhook handler will receive when a task transitions from 'submitted' to a terminal or intermediate state.",
- "type": "object",
- "properties": {
- "operation_id": {
- "type": "string",
- "description": "Publisher-defined operation identifier correlating a sequence of task updates across webhooks."
- },
- "task_id": {
- "type": "string",
- "description": "Unique identifier for this task. Use this to correlate webhook notifications with the original task submission."
- },
- "task_type": {
- "$ref": "/schemas/enums/task-type.json",
- "description": "Type of AdCP operation that triggered this webhook. Enables webhook handlers to route to appropriate processing logic."
- },
- "domain": {
- "$ref": "/schemas/enums/adcp-domain.json",
- "description": "AdCP domain this task belongs to. Helps classify the operation type at a high level."
- },
- "status": {
- "$ref": "/schemas/enums/task-status.json",
- "description": "Current task status. Webhooks are only triggered for status changes after initial submission (e.g., submitted → input-required, submitted → completed, submitted → failed)."
- },
- "timestamp": {
- "type": "string",
- "format": "date-time",
- "description": "ISO 8601 timestamp when this webhook was generated."
- },
- "message": {
- "type": "string",
- "description": "Human-readable summary of the current task state. Provides context about what happened and what action may be needed."
- },
- "context_id": {
- "type": "string",
- "description": "Session/conversation identifier. Use this to continue the conversation if input-required status needs clarification or additional parameters."
- },
- "progress": {
- "type": "object",
- "description": "Progress information for tasks still in 'working' state. Rarely seen in webhooks since 'working' tasks typically complete synchronously, but may appear if a task transitions from 'submitted' to 'working'.",
- "properties": {
- "percentage": {
- "type": "number",
- "minimum": 0,
- "maximum": 100,
- "description": "Completion percentage (0-100)"
- },
- "current_step": {
- "type": "string",
- "description": "Current step or phase of the operation"
- },
- "total_steps": {
- "type": "integer",
- "minimum": 1,
- "description": "Total number of steps in the operation"
- },
- "step_number": {
- "type": "integer",
- "minimum": 1,
- "description": "Current step number"
- }
- },
- "additionalProperties": false
- },
- "result": {
- "type": ["object"],
- "description": "Task-specific payload for this status update. Validated against the appropriate response schema based on task_type."
- },
-
- "error": {
- "type": ["string", "null"],
- "description": "Error message for failed tasks. Only present when status is 'failed'."
- }
- },
- "required": ["task_id", "task_type", "status", "timestamp"],
- "additionalProperties": true,
- "allOf": [
- {
- "if": {
- "properties": {
- "task_type": {"const": "create_media_buy"}
- }
- },
- "then": {
- "properties": {
- "result": {
- "$ref": "/schemas/media-buy/create-media-buy-response.json"
- }
- }
- }
- },
- {
- "if": {
- "properties": {
- "task_type": {"const": "update_media_buy"}
- }
- },
- "then": {
- "properties": {
- "result": {
- "$ref": "/schemas/media-buy/update-media-buy-response.json"
- }
- }
- }
- },
- {
- "if": {
- "properties": {
- "task_type": {"const": "sync_creatives"}
- }
- },
- "then": {
- "properties": {
- "result": {
- "$ref": "/schemas/media-buy/sync-creatives-response.json"
- }
- }
- }
- },
- {
- "if": {
- "properties": {
- "task_type": {"const": "activate_signal"}
- }
- },
- "then": {
- "properties": {
- "result": {
- "$ref": "/schemas/signals/activate-signal-response.json"
- }
- }
- }
- },
- {
- "if": {
- "properties": {
- "task_type": {"const": "get_signals"}
- }
- },
- "then": {
- "properties": {
- "result": {
- "$ref": "/schemas/signals/get-signals-response.json"
- }
- }
- }
- }
- ],
- "notes": [
- "Webhooks are ONLY triggered when the initial response status is 'submitted' (long-running operations)",
- "Webhook payloads include protocol-level fields (operation_id, task_type, status, optional task_id/context_id/timestamp/message) and the task-specific payload nested under 'result'",
- "The task-specific response data is NOT merged at the top level; it is contained entirely within the 'result' field",
- "For example, a create_media_buy webhook will include operation_id, task_type, status, and result.buyer_ref, result.media_buy_id, result.packages, etc.",
- "Your webhook handler receives the complete information needed to process the result without making additional API calls"
- ],
- "examples": [
- {
- "description": "Webhook for input-required status (human approval needed)",
- "data": {
- "operation_id": "op_456",
- "task_id": "task_456",
- "task_type": "create_media_buy",
- "domain": "media-buy",
- "status": "input-required",
- "timestamp": "2025-01-22T10:15:00Z",
- "context_id": "ctx_abc123",
- "message": "Campaign budget $150K requires VP approval to proceed",
- "result": {
- "errors": [
- {
- "code": "APPROVAL_REQUIRED",
- "message": "Budget exceeds auto-approval threshold of $100K. Awaiting VP approval before media buy creation.",
- "field": "packages[0].budget"
- }
- ]
- }
- }
- },
- {
- "description": "Webhook for completed create_media_buy",
- "data": {
- "operation_id": "op_456",
- "task_id": "task_456",
- "task_type": "create_media_buy",
- "domain": "media-buy",
- "status": "completed",
- "timestamp": "2025-01-22T10:30:00Z",
- "message": "Media buy created successfully with 2 packages ready for creative assignment",
- "result": {
- "media_buy_id": "mb_12345",
- "buyer_ref": "nike_q1_campaign_2024",
- "creative_deadline": "2024-01-30T23:59:59Z",
- "packages": [
- {
- "package_id": "pkg_12345_001",
- "buyer_ref": "nike_ctv_package",
- "product_id": "ctv_sports_premium",
- "budget": 60000,
- "pacing": "even",
- "pricing_option_id": "cpm-fixed-sports",
- "paused": false,
- "creative_assignments": [],
- "format_ids_to_provide": [
- {
- "agent_url": "https://creative.adcontextprotocol.org",
- "id": "video_standard_30s"
- }
- ]
- }
- ]
- }
- }
- },
- {
- "description": "Webhook for failed sync_creatives",
- "data": {
- "operation_id": "op_789",
- "task_id": "task_789",
- "task_type": "sync_creatives",
- "domain": "media-buy",
- "status": "failed",
- "timestamp": "2025-01-22T10:46:00Z",
- "message": "Creative sync failed due to invalid asset URLs",
- "error": "invalid_assets: One or more creative assets could not be accessed"
- }
- }
- ]
-}
diff --git a/static/schemas/source/index.json b/static/schemas/source/index.json
index 2e2c00fac..641737e59 100644
--- a/static/schemas/source/index.json
+++ b/static/schemas/source/index.json
@@ -133,9 +133,9 @@
"$ref": "/schemas/core/placement.json",
"description": "Represents a specific ad placement within a product's inventory"
},
- "webhook-payload": {
- "$ref": "/schemas/core/webhook-payload.json",
- "description": "Webhook payload structure sent when async task status changes - protocol-level fields at top-level (operation_id, task_type, status, etc.) and task-specific payload nested under 'result'"
+ "mcp-webhook-payload": {
+ "$ref": "/schemas/core/mcp-webhook-payload.json",
+ "description": "MCP-specific webhook payload structure for HTTP-based push notifications. Protocol-level fields at top-level (task_id, status, etc.) and AdCP data layer nested under 'result'. NOT used in A2A (uses native statusUpdate)."
},
"destination": {
"$ref": "/schemas/core/destination.json",
diff --git a/static/schemas/source/media-buy/create-media-buy-async-response-input-required.json b/static/schemas/source/media-buy/create-media-buy-async-response-input-required.json
new file mode 100644
index 000000000..4ff190929
--- /dev/null
+++ b/static/schemas/source/media-buy/create-media-buy-async-response-input-required.json
@@ -0,0 +1,31 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/media-buy/create-media-buy-async-response-input-required.json",
+ "title": "Create Media Buy - Input Required",
+ "description": "Payload when task is paused waiting for user input or approval.",
+ "type": "object",
+ "properties": {
+ "reason": {
+ "type": "string",
+ "enum": [
+ "APPROVAL_REQUIRED",
+ "BUDGET_EXCEEDS_LIMIT"
+ ],
+ "description": "Reason code indicating why input is needed"
+ },
+ "errors": {
+ "type": "array",
+ "description": "Optional validation errors or warnings for debugging purposes. Helps explain why input is required.",
+ "items": {
+ "$ref": "/schemas/core/error.json"
+ }
+ },
+ "context": {
+ "$ref": "/schemas/core/context.json"
+ },
+ "ext": {
+ "$ref": "/schemas/core/ext.json"
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/static/schemas/source/media-buy/create-media-buy-async-response-submitted.json b/static/schemas/source/media-buy/create-media-buy-async-response-submitted.json
new file mode 100644
index 000000000..4df6de4bb
--- /dev/null
+++ b/static/schemas/source/media-buy/create-media-buy-async-response-submitted.json
@@ -0,0 +1,17 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/media-buy/create-media-buy-async-response-submitted.json",
+ "title": "Create Media Buy - Submitted",
+ "description": "Payload acknowledging the task is queued. Usually empty or just context.",
+ "type": "object",
+ "properties": {
+ "context": {
+ "$ref": "/schemas/core/context.json"
+ },
+ "ext": {
+ "$ref": "/schemas/core/ext.json"
+ }
+ },
+ "additionalProperties": false
+}
+
diff --git a/static/schemas/source/media-buy/create-media-buy-async-response-working.json b/static/schemas/source/media-buy/create-media-buy-async-response-working.json
new file mode 100644
index 000000000..bac431e94
--- /dev/null
+++ b/static/schemas/source/media-buy/create-media-buy-async-response-working.json
@@ -0,0 +1,37 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/media-buy/create-media-buy-async-response-working.json",
+ "title": "Create Media Buy - Working",
+ "description": "Progress payload for active create_media_buy task.",
+ "type": "object",
+ "properties": {
+ "percentage": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 100,
+ "description": "Completion percentage (0-100)"
+ },
+ "current_step": {
+ "type": "string",
+ "description": "Current step or phase of the operation"
+ },
+ "total_steps": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Total number of steps in the operation"
+ },
+ "step_number": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Current step number"
+ },
+ "context": {
+ "$ref": "/schemas/core/context.json"
+ },
+ "ext": {
+ "$ref": "/schemas/core/ext.json"
+ }
+ },
+ "additionalProperties": false
+}
+
diff --git a/static/schemas/source/media-buy/get-products-async-response-input-required.json b/static/schemas/source/media-buy/get-products-async-response-input-required.json
new file mode 100644
index 000000000..2edb3ae32
--- /dev/null
+++ b/static/schemas/source/media-buy/get-products-async-response-input-required.json
@@ -0,0 +1,39 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/media-buy/get-products-async-response-input-required.json",
+ "title": "Get Products - Input Required",
+ "description": "Payload when search is paused waiting for user clarification.",
+ "type": "object",
+ "properties": {
+ "reason": {
+ "type": "string",
+ "enum": [
+ "CLARIFICATION_NEEDED",
+ "BUDGET_REQUIRED"
+ ],
+ "description": "Reason code indicating why input is needed"
+ },
+ "partial_results": {
+ "type": "array",
+ "description": "Partial product results that may help inform the clarification",
+ "items": {
+ "$ref": "/schemas/core/product.json"
+ }
+ },
+ "suggestions": {
+ "type": "array",
+ "description": "Suggested values or options for the required input",
+ "items": {
+ "type": "string"
+ }
+ },
+ "context": {
+ "$ref": "/schemas/core/context.json"
+ },
+ "ext": {
+ "$ref": "/schemas/core/ext.json"
+ }
+ },
+ "additionalProperties": false
+}
+
diff --git a/static/schemas/source/media-buy/get-products-async-response-submitted.json b/static/schemas/source/media-buy/get-products-async-response-submitted.json
new file mode 100644
index 000000000..24872f3d2
--- /dev/null
+++ b/static/schemas/source/media-buy/get-products-async-response-submitted.json
@@ -0,0 +1,22 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/media-buy/get-products-async-response-submitted.json",
+ "title": "Get Products - Submitted",
+ "description": "Payload acknowledging the search is queued. Usually for custom/bespoke product curation.",
+ "type": "object",
+ "properties": {
+ "estimated_completion": {
+ "type": "string",
+ "format": "date-time",
+ "description": "Estimated completion time for the search"
+ },
+ "context": {
+ "$ref": "/schemas/core/context.json"
+ },
+ "ext": {
+ "$ref": "/schemas/core/ext.json"
+ }
+ },
+ "additionalProperties": false
+}
+
diff --git a/static/schemas/source/media-buy/get-products-async-response-working.json b/static/schemas/source/media-buy/get-products-async-response-working.json
new file mode 100644
index 000000000..9682bfd9f
--- /dev/null
+++ b/static/schemas/source/media-buy/get-products-async-response-working.json
@@ -0,0 +1,35 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/media-buy/get-products-async-response-working.json",
+ "title": "Get Products - Working",
+ "description": "Progress payload for active get_products task.",
+ "type": "object",
+ "properties": {
+ "percentage": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 100,
+ "description": "Progress percentage of the search operation"
+ },
+ "current_step": {
+ "type": "string",
+ "description": "Current step in the search process (e.g., 'searching_inventory', 'validating_availability')"
+ },
+ "total_steps": {
+ "type": "integer",
+ "description": "Total number of steps in the search process"
+ },
+ "step_number": {
+ "type": "integer",
+ "description": "Current step number (1-indexed)"
+ },
+ "context": {
+ "$ref": "/schemas/core/context.json"
+ },
+ "ext": {
+ "$ref": "/schemas/core/ext.json"
+ }
+ },
+ "additionalProperties": false
+}
+
diff --git a/static/schemas/source/media-buy/sync-creatives-async-response-input-required.json b/static/schemas/source/media-buy/sync-creatives-async-response-input-required.json
new file mode 100644
index 000000000..487f8f961
--- /dev/null
+++ b/static/schemas/source/media-buy/sync-creatives-async-response-input-required.json
@@ -0,0 +1,26 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/media-buy/sync-creatives-async-response-input-required.json",
+ "title": "Sync Creatives - Input Required",
+ "description": "Payload when sync_creatives task is paused waiting for user input or approval.",
+ "type": "object",
+ "properties": {
+ "reason": {
+ "type": "string",
+ "enum": [
+ "APPROVAL_REQUIRED",
+ "ASSET_CONFIRMATION",
+ "FORMAT_CLARIFICATION"
+ ],
+ "description": "Reason code indicating why input is needed"
+ },
+ "context": {
+ "$ref": "/schemas/core/context.json"
+ },
+ "ext": {
+ "$ref": "/schemas/core/ext.json"
+ }
+ },
+ "additionalProperties": false
+}
+
diff --git a/static/schemas/source/media-buy/sync-creatives-async-response-submitted.json b/static/schemas/source/media-buy/sync-creatives-async-response-submitted.json
new file mode 100644
index 000000000..076a99445
--- /dev/null
+++ b/static/schemas/source/media-buy/sync-creatives-async-response-submitted.json
@@ -0,0 +1,17 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/media-buy/sync-creatives-async-response-submitted.json",
+ "title": "Sync Creatives - Submitted",
+ "description": "Payload acknowledging sync_creatives task is queued for processing.",
+ "type": "object",
+ "properties": {
+ "context": {
+ "$ref": "/schemas/core/context.json"
+ },
+ "ext": {
+ "$ref": "/schemas/core/ext.json"
+ }
+ },
+ "additionalProperties": false
+}
+
diff --git a/static/schemas/source/media-buy/sync-creatives-async-response-working.json b/static/schemas/source/media-buy/sync-creatives-async-response-working.json
new file mode 100644
index 000000000..4c856385e
--- /dev/null
+++ b/static/schemas/source/media-buy/sync-creatives-async-response-working.json
@@ -0,0 +1,47 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/media-buy/sync-creatives-async-response-working.json",
+ "title": "Sync Creatives - Working",
+ "description": "Progress payload for active sync_creatives task.",
+ "type": "object",
+ "properties": {
+ "percentage": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 100,
+ "description": "Completion percentage (0-100)"
+ },
+ "current_step": {
+ "type": "string",
+ "description": "Current step or phase of the operation"
+ },
+ "total_steps": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Total number of steps in the operation"
+ },
+ "step_number": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Current step number"
+ },
+ "creatives_processed": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Number of creatives processed so far"
+ },
+ "creatives_total": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Total number of creatives to process"
+ },
+ "context": {
+ "$ref": "/schemas/core/context.json"
+ },
+ "ext": {
+ "$ref": "/schemas/core/ext.json"
+ }
+ },
+ "additionalProperties": false
+}
+
diff --git a/static/schemas/source/media-buy/update-media-buy-async-response-input-required.json b/static/schemas/source/media-buy/update-media-buy-async-response-input-required.json
new file mode 100644
index 000000000..e1d36b5df
--- /dev/null
+++ b/static/schemas/source/media-buy/update-media-buy-async-response-input-required.json
@@ -0,0 +1,25 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/media-buy/update-media-buy-async-response-input-required.json",
+ "title": "Update Media Buy - Input Required",
+ "description": "Payload when update_media_buy task is paused waiting for user input or approval.",
+ "type": "object",
+ "properties": {
+ "reason": {
+ "type": "string",
+ "enum": [
+ "APPROVAL_REQUIRED",
+ "CHANGE_CONFIRMATION"
+ ],
+ "description": "Reason code indicating why input is needed"
+ },
+ "context": {
+ "$ref": "/schemas/core/context.json"
+ },
+ "ext": {
+ "$ref": "/schemas/core/ext.json"
+ }
+ },
+ "additionalProperties": false
+}
+
diff --git a/static/schemas/source/media-buy/update-media-buy-async-response-submitted.json b/static/schemas/source/media-buy/update-media-buy-async-response-submitted.json
new file mode 100644
index 000000000..6cc21e02f
--- /dev/null
+++ b/static/schemas/source/media-buy/update-media-buy-async-response-submitted.json
@@ -0,0 +1,17 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/media-buy/update-media-buy-async-response-submitted.json",
+ "title": "Update Media Buy - Submitted",
+ "description": "Payload acknowledging update_media_buy task is queued for processing.",
+ "type": "object",
+ "properties": {
+ "context": {
+ "$ref": "/schemas/core/context.json"
+ },
+ "ext": {
+ "$ref": "/schemas/core/ext.json"
+ }
+ },
+ "additionalProperties": false
+}
+
diff --git a/static/schemas/source/media-buy/update-media-buy-async-response-working.json b/static/schemas/source/media-buy/update-media-buy-async-response-working.json
new file mode 100644
index 000000000..feddaf870
--- /dev/null
+++ b/static/schemas/source/media-buy/update-media-buy-async-response-working.json
@@ -0,0 +1,37 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/media-buy/update-media-buy-async-response-working.json",
+ "title": "Update Media Buy - Working",
+ "description": "Progress payload for active update_media_buy task.",
+ "type": "object",
+ "properties": {
+ "percentage": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 100,
+ "description": "Completion percentage (0-100)"
+ },
+ "current_step": {
+ "type": "string",
+ "description": "Current step or phase of the operation"
+ },
+ "total_steps": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Total number of steps in the operation"
+ },
+ "step_number": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Current step number"
+ },
+ "context": {
+ "$ref": "/schemas/core/context.json"
+ },
+ "ext": {
+ "$ref": "/schemas/core/ext.json"
+ }
+ },
+ "additionalProperties": false
+}
+