Skip to main content
MCP と A2A はどちらも非同期タスク更新に HTTP Webhook を利用します。ポーリングの代わりに Webhook URL を指定すると、サーバーがステータス変更を直接 POST します。

プロトコル比較

AspectMCPA2A
Spec StatusAdCP が規定プロトコル標準機能
ConfigurationpushNotificationConfigpushNotificationConfig
Envelopemcp-webhook-payload.jsonTask または TaskStatusUpdateEvent
Data Locationresult フィールドstatus.message.parts[].data
Data Schemas同一 の AdCP スキーマ同一 の AdCP スキーマ

Webhook 設定

MCP Webhooks

MCP はプッシュ通知を定義していません。AdCP が pushNotificationConfig とペイロード形式を規定して補完します。
const response = await session.call('create_media_buy',
  { /* task params */ },
  {
    pushNotificationConfig: {
      url: "https://buyer.com/webhooks/adcp",
      authentication: {
        schemes: ["HMAC-SHA256"],
        credentials: "shared_secret_32_chars"
      }
    }
  }
);

A2A Webhooks

A2A はプッシュ通知を標準で定義しています:
await a2a.send({
  message: {
    parts: [{
      kind: "data",
      data: {
        skill: "create_media_buy",
        parameters: { /* task params */ }
      }
    }]
  },
  pushNotificationConfig: {
    url: "https://buyer.com/webhooks/a2a",
    authentication: {
      schemes: ["bearer"],
      credentials: "shared_secret_32_chars"
    }
  }
});

Webhook が送信される条件

次の すべて を満たすと Webhook が送信されます:
  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
初回レスポンスがすでに終端(completed, failed, rejected)なら Webhook は送信されません(すでに結果が手元にあるため)。 Webhook を送るステータス変化:
  • working → 進捗更新
  • input-required → 人の入力が必要
  • completed → 最終結果
  • failed → エラー詳細
  • canceled → キャンセル確定

Webhook ペイロード形式

MCP ペイロード

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

{
  "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" }
    ]
  }
}

A2A ペイロード

A2A sends Task (for final states) or TaskStatusUpdateEvent (for progress updates):
{
  "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"
  }
}

ステータス別データスキーマ

StatusData SchemaContents
completed[task]-response.json成功ブランチの完全レスポンス
failed[task]-response.jsonエラーブランチの完全レスポンス
working[task]-async-response-working.json進捗情報(percentage, step
input-required[task]-async-response-input-required.json必要事項、承認データ
submitted[task]-async-response-submitted.json受領通知(通常最小限)

Webhook 認証

AdCP は A2A の PushNotificationConfig を Webhook 設定に採用しています:
{
  "push_notification_config": {
    "url": "https://buyer.example.com/webhooks/adcp",
    "authentication": {
      "schemes": ["Bearer"],
      "credentials": "secret_token_min_32_chars"
    }
  }
}

対応する認証方式

Bearer Token(シンプル、開発に推奨)
{
  "authentication": {
    "schemes": ["Bearer"],
    "credentials": "secret_token_32_chars"
  }
}
HMAC Signature(エンタープライズ、本番に推奨)
{
  "authentication": {
    "schemes": ["HMAC-SHA256"],
    "credentials": "shared_secret_32_chars"
  }
}

パブリッシャー側実装例(Bearer)

const config = pushNotificationConfig;
const scheme = config.authentication.schemes[0];

if (scheme === 'Bearer') {
  await axios.post(config.url, payload, {
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${config.authentication.credentials}`
    }
  });
}

パブリッシャー側実装例(HMAC-SHA256)

if (scheme === 'HMAC-SHA256') {
  const timestamp = new Date().toISOString();
  const signature = crypto
    .createHmac('sha256', config.authentication.credentials)
    .update(timestamp + JSON.stringify(payload))
    .digest('hex');

  await axios.post(config.url, payload, {
    headers: {
      'Content-Type': 'application/json',
      'X-ADCP-Signature': `sha256=${signature}`,
      'X-ADCP-Timestamp': timestamp
    }
  });
}

Buyer Implementation (Bearer)

app.post('/webhooks/adcp', async (req, res) => {
  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.ADCP_WEBHOOK_TOKEN) {
    return res.status(401).json({ error: 'Invalid token' });
  }

  await processWebhook(req.body);
  res.status(200).json({ status: 'processed' });
});

Buyer Implementation (HMAC-SHA256)

app.post('/webhooks/adcp', async (req, res) => {
  const signature = req.headers['x-adcp-signature'];
  const timestamp = req.headers['x-adcp-timestamp'];

  if (!signature || !timestamp) {
    return res.status(401).json({ error: 'Missing signature headers' });
  }

  // Reject old webhooks (prevent replay attacks)
  const eventTime = new Date(timestamp);
  if (Date.now() - eventTime > 5 * 60 * 1000) {
    return res.status(401).json({ error: 'Webhook too old' });
  }

  // Verify signature
  const expectedSig = crypto
    .createHmac('sha256', process.env.ADCP_WEBHOOK_SECRET)
    .update(timestamp + JSON.stringify(req.body))
    .digest('hex');

  if (signature !== `sha256=${expectedSig}`) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  await processWebhook(req.body);
  res.status(200).json({ status: 'processed' });
});

Webhook Reliability

Delivery Semantics

AdCP webhooks use at-least-once delivery semantics:
  • Not guaranteed: Webhooks may fail due to network issues, server downtime, or configuration problems
  • May be duplicated: The same event might be delivered multiple times
  • May arrive out of order: Later events could arrive before earlier ones
  • Timeout behavior: Webhook delivery has limited retry attempts and timeouts

Retry Strategy

Publishers should use exponential backoff with jitter:
class WebhookDelivery {
  constructor() {
    this.maxRetries = 3;
    this.baseDelay = 1000; // 1 second
    this.maxDelay = 60000; // 1 minute
  }

  async deliverWithRetry(url, payload, attempt = 0) {
    try {
      const response = await this.sendWebhook(url, payload);

      if (response.status >= 200 && response.status < 300) {
        return { success: true, attempts: attempt + 1 };
      }

      // Retry on 5xx errors and timeouts
      if (response.status >= 500 && attempt < this.maxRetries) {
        await this.delayWithJitter(attempt);
        return this.deliverWithRetry(url, payload, attempt + 1);
      }

      // Don't retry 4xx errors (client errors)
      return { success: false, error: 'Client error', attempts: attempt + 1 };

    } catch (error) {
      if (attempt < this.maxRetries) {
        await this.delayWithJitter(attempt);
        return this.deliverWithRetry(url, payload, attempt + 1);
      }
      return { success: false, error: error.message, attempts: attempt + 1 };
    }
  }

  async delayWithJitter(attempt) {
    const exponentialDelay = Math.min(
      this.baseDelay * Math.pow(2, attempt),
      this.maxDelay
    );
    // Add ±25% jitter to prevent thundering herd
    const jitter = exponentialDelay * (0.75 + Math.random() * 0.5);
    await new Promise(resolve => setTimeout(resolve, jitter));
  }
}
Retry Schedule:
  • Attempt 1: Immediate
  • Attempt 2: After ~1 second (with jitter)
  • Attempt 3: After ~2 seconds (with jitter)
  • Attempt 4: After ~4 seconds (with jitter)
  • Give up after 4 total attempts

Circuit Breaker Pattern

Publishers must implement circuit breakers to prevent webhook queues from growing unbounded:
class CircuitBreaker {
  constructor(endpoint) {
    this.endpoint = endpoint;
    this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
    this.failureCount = 0;
    this.failureThreshold = 5;
    this.successThreshold = 2;
    this.timeout = 60000; // 1 minute
    this.halfOpenTime = null;
    this.successCount = 0;
  }

  async execute(fn) {
    if (this.state === 'OPEN') {
      // Check if circuit should move to HALF_OPEN
      if (Date.now() - this.halfOpenTime > this.timeout) {
        this.state = 'HALF_OPEN';
        this.successCount = 0;
      } else {
        throw new Error('Circuit breaker is OPEN');
      }
    }

    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  onSuccess() {
    this.failureCount = 0;

    if (this.state === 'HALF_OPEN') {
      this.successCount++;
      if (this.successCount >= this.successThreshold) {
        this.state = 'CLOSED';
        console.log(`Circuit breaker CLOSED for ${this.endpoint}`);
      }
    }
  }

  onFailure() {
    this.failureCount++;

    if (this.failureCount >= this.failureThreshold) {
      this.state = 'OPEN';
      this.halfOpenTime = Date.now();
      console.error(`Circuit breaker OPEN for ${this.endpoint}`);
    }
  }
}
Circuit Breaker States:
  • CLOSED: Normal operation, webhooks delivered
  • OPEN: Endpoint is down, webhooks are dropped (not queued)
  • HALF_OPEN: Testing if endpoint recovered, limited webhooks sent

Queue Management

Publishers should implement bounded queues with overflow policies:
class BoundedWebhookQueue {
  constructor(maxSize = 1000) {
    this.maxSize = maxSize;
    this.queue = [];
    this.droppedCount = 0;
  }

  enqueue(webhook) {
    if (this.queue.length >= this.maxSize) {
      // Overflow policy: drop oldest webhooks
      const dropped = this.queue.shift();
      this.droppedCount++;
      console.warn(`Dropped webhook ${dropped.id} due to queue overflow`);
    }
    this.queue.push(webhook);
  }
}
Best Practices:
  • Set max queue size based on available memory and recovery time
  • Monitor queue depth and dropped webhook counts
  • Alert operations when queues are consistently full
  • Use dead letter queues for manual investigation of persistent failures
  • Implement queue per buyer endpoint (not global queue)

Idempotent Webhook Handlers

Always implement idempotent handlers that can safely process the same event multiple times:
app.post('/webhooks/adcp', async (req, res) => {
  const { task_id, current_status, timestamp, event_id } = req.body;

  // Idempotent check - avoid duplicate processing
  const existing = await db.getWebhookEvent(event_id);
  if (existing) {
    console.log(`Webhook ${event_id} already processed`);
    return res.status(200).json({ status: 'already_processed' });
  }

  // Record this webhook event
  await db.recordWebhookEvent(event_id, timestamp);

  // Process the status change
  await processTaskStatusChange(task_id, current_status, timestamp);

  // Always return 200 for successful processing
  res.status(200).json({ status: 'processed' });
});

Sequence Handling

Use timestamps to ensure proper event ordering:
async function processTaskStatusChange(taskId, newStatus, timestamp) {
  const currentTask = await db.getTask(taskId);

  // Ignore out-of-order events
  if (currentTask?.updated_at >= timestamp) {
    console.log(`Ignoring out-of-order webhook for task ${taskId}`);
    return;
  }

  // Update task with new status
  await db.updateTask(taskId, {
    status: newStatus,
    updated_at: timestamp
  });

  // Trigger any business logic
  await handleStatusChange(taskId, newStatus);
}

Polling as Backup

Never rely solely on webhooks. Use polling as a reliable backup:
class TaskTracker {
  constructor() {
    this.pendingTasks = new Map();
    this.pollInterval = 30000; // 30 seconds
  }

  async trackTask(taskId, webhookConfigured = false) {
    this.pendingTasks.set(taskId, {
      lastPolled: Date.now(),
      webhookConfigured,
      pollAttempts: 0
    });

    // Start polling backup even if webhook is configured
    this.schedulePolling(taskId);
  }

  async schedulePolling(taskId) {
    const task = this.pendingTasks.get(taskId);
    if (!task) return;

    // Increase polling interval if webhook is configured
    const interval = task.webhookConfigured ?
      this.pollInterval * 4 : // 2 minutes with webhook
      this.pollInterval;      // 30 seconds without webhook

    setTimeout(async () => {
      if (this.pendingTasks.has(taskId)) {
        await this.pollTask(taskId);
        this.schedulePolling(taskId); // Continue polling
      }
    }, interval);
  }

  async pollTask(taskId) {
    try {
      const response = await adcp.call('tasks/get', {
        task_id: taskId,
        include_result: true
      });

      await this.updateTaskState(taskId, response);

      // Stop tracking if complete
      if (['completed', 'failed', 'canceled'].includes(response.status)) {
        this.pendingTasks.delete(taskId);
      }

    } catch (error) {
      console.error(`Polling failed for task ${taskId}:`, error);
    }
  }
}

Reporting Webhooks

In addition to task status webhooks, AdCP supports reporting webhooks for automated delivery performance notifications.

Configuration

{
  "buyer_ref": "campaign_2024",
  "reporting_webhook": {
    "url": "https://buyer.example.com/webhooks/reporting",
    "auth_type": "bearer",
    "auth_token": "secret_token",
    "reporting_frequency": "daily"
  }
}

Payload Structure

{
  "notification_type": "scheduled",
  "sequence_number": 5,
  "next_expected_at": "2024-02-06T08:00:00Z",
  "reporting_period": {
    "start": "2024-02-05T00:00:00Z",
    "end": "2024-02-05T23:59:59Z"
  },
  "currency": "USD",
  "media_buy_deliveries": [
    {
      "media_buy_id": "mb_001",
      "buyer_ref": "campaign_a",
      "status": "active",
      "totals": {...},
      "by_package": [...]
    }
  ]
}

Implementation Requirements

  1. Array Handling: Always process media_buy_deliveries as an array (may contain 1 to N media buys)
  2. Idempotent Processing: Same as task webhooks - handle duplicates safely
  3. Sequence Tracking: Use sequence_number to detect gaps or out-of-order delivery
  4. Fallback Strategy: Continue polling get_media_buy_delivery as backup
  5. Delay Handling: Treat "delayed" notifications as normal, not errors

Best Practices Summary

  1. Always implement polling backup - Don’t rely solely on webhooks
  2. Handle duplicates gracefully - Use idempotent processing with event IDs
  3. Check timestamps - Ignore out-of-order events based on timestamps
  4. Return 200 quickly - Acknowledge webhook receipt immediately
  5. Verify authenticity - Always validate webhook signatures
  6. Log webhook events - Keep audit trail for debugging
  7. Set reasonable timeouts - Don’t wait forever for webhook delivery
  8. Graceful degradation - Fall back to polling if webhooks consistently fail

Next Steps