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 +} +