diff --git a/docs/media-buy/task-reference/create_media_buy.md b/docs/media-buy/task-reference/create_media_buy.md index 3ca860dc3..76b4dc1a6 100644 --- a/docs/media-buy/task-reference/create_media_buy.md +++ b/docs/media-buy/task-reference/create_media_buy.md @@ -824,19 +824,69 @@ For MCP implementations using polling, use this endpoint to check the status of #### Option 2: Webhooks (MCP) -Register a callback URL to receive push notifications: -```json -{ - "tool": "create_media_buy", - "arguments": { - "buyer_ref": "campaign_2024", - "packages": [...], - "webhook_url": "https://buyer.example.com/mcp/webhooks", - "webhook_auth_token": "bearer-token-xyz" +Register a callback URL to receive push notifications for long-running operations. Webhooks are ONLY used when the initial response is `submitted`. + +**Configuration:** +```javascript +const response = await session.call('create_media_buy', + { + buyer_ref: "campaign_2024", + packages: [...] + }, + { + webhook_url: "https://buyer.example.com/webhooks/adcp/create_media_buy/agent_id/op_id", + webhook_auth: { type: "bearer", credentials: "bearer-token-xyz" } } +); +``` + +**Response patterns:** +- **`completed`** - Synchronous success, webhook NOT called (you have the result) +- **`working`** - Will complete within ~120s, webhook NOT called (wait for response) +- **`submitted`** - Long-running operation, webhook WILL be called on status changes + +**Example webhook flow (only for `submitted` operations):** + +Webhook POST for human approval needed: +```http +POST /webhooks/adcp/create_media_buy/agent_id/op_id HTTP/1.1 +Host: buyer.example.com +Authorization: Bearer bearer-token-xyz +Content-Type: application/json + +{ + "adcp_version": "1.6.0", + "status": "input-required", + "task_id": "task_456", + "buyer_ref": "campaign_2024", + "message": "Campaign budget $150K requires approval to proceed" +} +``` + +**Webhook POST when complete (after approval - full create_media_buy response):** +```http +POST /webhooks/adcp/create_media_buy/agent_id/op_id HTTP/1.1 +Host: buyer.example.com +Authorization: Bearer bearer-token-xyz +Content-Type: application/json + +{ + "adcp_version": "1.6.0", + "status": "completed", + "media_buy_id": "mb_12345", + "buyer_ref": "campaign_2024", + "creative_deadline": "2024-01-30T23:59:59Z", + "packages": [ + { + "package_id": "pkg_001", + "buyer_ref": "ctv_package" + } + ] } ``` +Each webhook receives the full response object for that status. See **[Task Management: Webhook Integration](../../protocols/task-management.md#webhook-integration)** for complete details. + ### A2A Status Checking A2A supports both SSE streaming and webhooks as shown in the examples above. Choose based on your needs: diff --git a/docs/protocols/core-concepts.md b/docs/protocols/core-concepts.md index 491305c64..4ca067f21 100644 --- a/docs/protocols/core-concepts.md +++ b/docs/protocols/core-concepts.md @@ -326,12 +326,176 @@ await a2a.send({ ### Server Decision on Webhook Usage -The server always decides whether to use webhooks: +The server decides whether to use webhooks based on the initial response status: -- **Quick operations** (< 120s): Server returns `working`, ignores webhook -- **Long operations** (hours/days): Server returns `submitted`, uses webhook if provided +- **`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`. + +**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) + +### Webhook POST Format + +When an async operation changes status, the publisher POSTs the **complete task response object** to your webhook URL. + +#### Webhook Scenarios + +**Scenario 1: Synchronous completion (no webhook)** +```javascript +// Initial request +const response = await session.call('create_media_buy', params, { webhook_url: "..." }); + +// Response is immediate and complete - webhook is NOT called +{ + "status": "completed", + "media_buy_id": "mb_12345", + "packages": [...] +} +``` + +**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..." +} + +// 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 +``` + +**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" +}); + +// Response indicates long-running async operation +{ + "adcp_version": "1.6.0", + "status": "submitted", + "task_id": "task_456", + "buyer_ref": "nike_q1_campaign_2024", + "message": "Campaign requires sales approval. Expected time: 2-4 hours." +} + +// Later: Webhook POST when approval is needed +POST /webhooks/adcp/create_media_buy/agent_123/op_456 HTTP/1.1 +{ + "adcp_version": "1.6.0", + "status": "input-required", + "task_id": "task_456", + "buyer_ref": "nike_q1_campaign_2024", + "message": "Please approve $150K campaign to proceed" +} + +// Later: Webhook POST when approved and completed (full create_media_buy response) +POST /webhooks/adcp/create_media_buy/agent_123/op_456 HTTP/1.1 +{ + "adcp_version": "1.6.0", + "status": "completed", + "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" + } + ] +} +``` + +#### For Other Async Operations + +Each async operation posts its specific response schema: + +- **`activate_signal`** → `activate-signal-response.json` +- **`sync_creatives`** → `sync-creatives-response.json` +- **`update_media_buy`** → `update-media-buy-response.json` + +#### Webhook URL Patterns + +Structure your webhook URLs to identify the operation and agent: + +``` +https://buyer.com/webhooks/adcp/{task_name}/{agent_id}/{operation_id} +``` + +**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` + +Your webhook handler can parse the URL path to route to the correct handler based on the task name. + +#### Webhook Payload Structure + +Every webhook POST contains the complete task response for that status, matching the task's response schema. + +**`input-required` webhook (human needs to respond):** +```json +{ + "adcp_version": "1.6.0", + "status": "input-required", + "task_id": "task_456", + "buyer_ref": "nike_q1_campaign_2024", + "message": "Campaign budget requires VP approval to proceed" +} +``` + +**`completed` webhook (operation finished - full create_media_buy response):** +```json +{ + "adcp_version": "1.6.0", + "status": "completed", + "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 +{ + "adcp_version": "1.6.0", + "status": "failed", + "task_id": "task_456", + "buyer_ref": "nike_q1_campaign_2024", + "errors": [ + { + "code": "insufficient_inventory", + "message": "Requested targeting yielded 0 available impressions", + "suggestion": "Broaden geographic targeting or increase budget" + } + ] +} +``` + +**Key principle:** Webhooks are ONLY called for `submitted` operations, and each webhook contains the full response object matching the task's response schema. + ### Task State Reconciliation Use `tasks/list` to recover from lost state: diff --git a/docs/protocols/task-management.md b/docs/protocols/task-management.md index bb74d9424..9edcb5659 100644 --- a/docs/protocols/task-management.md +++ b/docs/protocols/task-management.md @@ -446,34 +446,135 @@ await a2a.send({ Task management integrates with protocol-level webhook configuration for push notifications. -### Webhook Events +### Webhook Configuration -AdCP sends webhook notifications for task status changes: +Configure webhooks at the protocol level when making async task calls. See **[Core Concepts: Protocol-Level Webhook Configuration](./core-concepts.md#protocol-level-webhook-configuration)** for complete setup examples. + +**Quick example:** +```javascript +const response = await session.call('create_media_buy', + { /* task params */ }, + { + webhook_url: "https://buyer.com/webhooks/adcp/create_media_buy/agent_id/operation_id", + webhook_auth: { type: "bearer", credentials: "secret" } + } +); +``` + +### Webhook POST Format + +When a task's status changes, the publisher POSTs the **complete task response object** to your webhook URL. + +**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 -```json { - "event_type": "task_status_changed", + "adcp_version": "1.6.0", + "status": "input-required", "task_id": "task_456", - "previous_status": "working", - "current_status": "completed", - "timestamp": "2025-01-22T10:25:00Z", - "task_type": "create_media_buy", - "domain": "media-buy", - "context": { - "buyer_ref": "nike_q1_2025" - }, - "result": { - // Included for completed tasks - "media_buy_id": "mb_987654321" - } + "buyer_ref": "nike_q1_campaign_2024", + "message": "Campaign budget $150K requires VP approval to proceed" } ``` +**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 + +{ + "adcp_version": "1.6.0", + "status": "completed", + "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 === 'media_buy') { + await handleMediaBuyCreated({ + media_buy_id: response.media_buy_id, + buyer_ref: response.buyer_ref, + packages: response.packages, + creative_deadline: response.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](./core-concepts.md#webhook-reliability)** for detailed implementation guidance including idempotent handlers, sequence handling, security considerations, and polling as backup. +See **[Core Concepts: Webhook Reliability](./core-concepts.md#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 ## Error Handling diff --git a/docs/signals/tasks/activate_signal.md b/docs/signals/tasks/activate_signal.md index 82ddf853e..05c21f7e5 100644 --- a/docs/signals/tasks/activate_signal.md +++ b/docs/signals/tasks/activate_signal.md @@ -185,10 +185,46 @@ data: {"status": {"state": "completed"}, "artifacts": [{ ``` ### Protocol Transport -- **MCP**: Returns task_id for polling-based asynchronous operation tracking +- **MCP**: Returns task_id for polling-based asynchronous operation tracking or webhook-based push notifications - **A2A**: Uses Server-Sent Events for real-time progress updates and completion - **Data Consistency**: Both protocols contain identical AdCP data structures and version information +### Webhook Support + +For long-running activations (when initial response is `submitted`), configure a webhook to receive the complete response when activation completes: + +```javascript +const response = await session.call('activate_signal', + { + signal_agent_segment_id: "luxury_auto_intenders", + platform: "the-trade-desk", + account: "agency-123-ttd" + }, + { + webhook_url: "https://buyer.com/webhooks/adcp/activate_signal/agent_id/op_id", + webhook_auth: { type: "bearer", credentials: "secret-token" } + } +); +``` + +When activation completes, you receive the full `activate_signal` response: + +```http +POST /webhooks/adcp/activate_signal/agent_id/op_id HTTP/1.1 +Content-Type: application/json +Authorization: Bearer secret-token + +{ + "adcp_version": "1.0.0", + "status": "deployed", + "task_id": "activation_789", + "decisioning_platform_segment_id": "ttd_agency123_lux_auto", + "deployed_at": "2025-01-15T14:30:00Z" +} +``` + +See **[Task Management: Webhook Integration](../../protocols/task-management.md#webhook-integration)** for complete details on webhook configuration and reliability. + ## Scenarios ### Initial Response (Pending)