Skip to main content
プッシュ通知により、パブリッシャーはポーリングを必要とせず、タスクのステータス更新を直接配信できます。タスクリクエストに Webhook URL を指定すると、パブリッシャーはタスクの進行に応じてその URL にステータス変更を POST します。

仕組み

  1. タスク呼び出しごとに一意のオペレーション ID が生成されます
  2. そのID(および他のルーティングパラメーター)を URL テンプレートに代入して Webhook URL が構築されます
  3. push_notification_config がその URL と HMAC 認証情報とともにタスクリクエストボディに注入されます
  4. タスクのステータスが変化するたびに、パブリッシャーが Webhook 通知を URL に POST します
  5. 各通知はペイロード内に operation_id をエコーバックするため、URL を解析せずに相関付けできます
create_media_buy request
  └── push_notification_config
        ├── url: "https://you.com/adcp/webhook/create_media_buy/agent_123/cd51e063-2b79-4a6d-afac-ed7789c3a443"
        └── authentication: { schemes: ["HMAC-SHA256"], credentials: "..." }

              ↓ publisher processes task ↓

POST https://you.com/adcp/webhook/create_media_buy/agent_123/cd51e063-2b79-4a6d-afac-ed7789c3a443
  {
    "task_id": "task_456",
    "operation_id": "cd51e063-2b79-4a6d-afac-ed7789c3a443",   ← echoed from your URL
    "status": "completed",
    "result": { ... }
  }
@adcp/client ライブラリを使用している場合、このフロー全体は自動的に処理される — クライアントに webhookUrlTemplatewebhookSecret を一度設定すると、push_notification_config がすべての送信タスク呼び出しに注入されます。

命名規則: snake_case vs camelCase

混乱しやすい点があります。2つの命名規則が存在する:
コンテキストフィールド名
MCP タスク引数(AdCP JSON)push_notification_config{ push_notification_config: { url: ... } }
A2A 設定オブジェクトpushNotificationConfigconfiguration: { pushNotificationConfig: { url: ... } }
AdCP のフィールド名は常に push_notification_config(snake_case)だ。他のタスクパラメーターと並んでタスクリクエストボディに含めます。 A2A の場合、A2A プロトコルが camelCase を使用して configuration エンベロープにラップするが、オブジェクトの内容は同一です。

リクエストへの push_notification_config の追加

MCP

push_notification_config をタスク引数として、他のタスクパラメーターとマージして含める:
{
  "brand": { "brand_id": "acme" },
  "start_time": { "type": "date", "date": "2025-03-01" },
  "end_time": "2025-06-30T23:59:59Z",
  "packages": [...],
  "push_notification_config": {
    "url": "https://you.com/webhooks/adcp/create_media_buy/op_abc123",
    "authentication": {
      "schemes": ["HMAC-SHA256"],
      "credentials": "your_shared_secret_min_32_chars"
    }
  }
}

A2A

A2A の場合、スキルパラメーターは message.parts[].data.parameters に格納します。プッシュ通知設定はトップレベルの configuration オブジェクトに入れる:
{
  "message": {
    "parts": [{
      "kind": "data",
      "data": {
        "skill": "create_media_buy",
        "parameters": {
          "packages": [...]
        }
      }
    }]
  },
  "configuration": {
    "pushNotificationConfig": {
      "url": "https://you.com/webhooks/adcp/create_media_buy/op_abc123",
      "authentication": {
        "schemes": ["HMAC-SHA256"],
        "credentials": "your_shared_secret_min_32_chars"
      }
    }
  }
}

オペレーション ID と URL テンプレート

オペレーション ID により、受信した Webhook を適切なハンドラーにルーティングできます。典型的なパターン:
  1. タスク呼び出しごとに一意の ID を生成します
  2. Webhook URL パスに埋め込む
  3. パブリッシャーがペイロードで operation_id をエコーする — URL 解析は不要
URL テンプレートパターン:
https://you.com/webhooks/{task_type}/{agent_id}/{operation_id}
例(クライアントライブラリが自動処理):
import { randomUUID } from 'crypto';

const operationId = randomUUID(); // e.g. "cd51e063-2b79-4a6d-afac-ed7789c3a443"
const webhookUrl = `https://you.com/adcp/webhook/create_media_buy/${agentId}/${operationId}`;

// pass webhookUrl in push_notification_config.url
パブリッシャーの Webhook ペイロードには "operation_id": "cd51e063-2b79-4a6d-afac-ed7789c3a443" が含まれるため、URL を解析せずに正しい保留オペレーションにルーティングできます。

Webhook が送信される条件

Webhook は、リクエストに push_notification_config が含まれている限り、最初のレスポンス以降の各ステータス変化に対して送信されます。 タスクが同期的に完了した場合(最初のレスポンスがすでに completed または failed)、Webhook は送信されない — すでに結果が手元にあるためです。 Webhook をトリガーするステータス変化:
Status意味
workingタスクが処理中 — 進捗情報が含まれることがある
input-required人の承認または確認待ち
completed最終結果が利用可能
failedタスクがエラー詳細とともに失敗
canceledタスクがキャンセルされた

Webhook ペイロード形式

MCP

{
  "task_id": "task_456",
  "operation_id": "cd51e063-2b79-4a6d-afac-ed7789c3a443",
  "task_type": "create_media_buy",
  "domain": "media-buy",
  "status": "completed",
  "timestamp": "2025-01-22T10:30:00Z",
  "message": "Media buy created successfully",
  "result": {
    "media_buy_id": "mb_12345",
    "packages": [
      { "package_id": "pkg_001", "context": { "line_item": "li_ctv_sports" } }
    ]
  }
}

A2A

A2A は最終状態に対して Task オブジェクト、進捗には TaskStatusUpdateEvent を送信します。AdCP の結果データは status.message.parts[].data に含まれます:
{
  "id": "task_456",
  "contextId": "ctx_123",
  "status": {
    "state": "completed",
    "message": {
      "role": "agent",
      "parts": [
        { "kind": "text", "text": "Media buy created successfully" },
        {
          "kind": "data",
          "data": {
            "media_buy_id": "mb_12345",
            "packages": [
              { "package_id": "pkg_001", "context": { "line_item": "li_ctv_sports" } }
            ]
          }
        }
      ]
    },
    "timestamp": "2025-01-22T10:30:00Z"
  }
}

プロトコル比較

MCPA2A
設定フィールドpush_notification_config(タスク引数内)configuration.pushNotificationConfig(スキルパラメーターとは別)
エンベロープmcp-webhook-payload.jsonネイティブの Task / TaskStatusUpdateEvent
結果の場所result フィールドstatus.message.parts[].data
データスキーマ同一の AdCP スキーマ同一の AdCP スキーマ

ステータス別結果データ

Statusresult / data の内容
completed / failed完全なタスクレスポンス
working進捗: percentagecurrent_steptotal_steps
input-required理由と任意のバリデーションエラー
submitted最小限の受付確認

認証

Bearer トークン

シンプルなトークン認証 — パブリッシャーが Authorization ヘッダーでトークンを送信します。 設定:
{
  "authentication": {
    "schemes": ["Bearer"],
    "credentials": "your_bearer_token_min_32_chars"
  }
}
サーバーサイドの検証:
app.post('/webhooks/adcp', (req, res) => {
  const token = req.headers.authorization?.replace('Bearer ', '');
  if (token !== process.env.ADCP_WEBHOOK_TOKEN) {
    return res.status(401).end();
  }
  processWebhook(req.body);
  res.status(200).end();
});

HMAC-SHA256(本番環境推奨)

パブリッシャーが共有シークレットで各リクエストに署名し、リプレイ保護のためのタイムスタンプを含めます。受信側は署名とタイムスタンプの両方を検証します。 設定:
{
  "authentication": {
    "schemes": ["HMAC-SHA256"],
    "credentials": "your_shared_secret_min_32_chars"
  }
}
パブリッシャーが送信する2つのヘッダー:
X-ADCP-Signature: sha256=<hex digest>
X-ADCP-Timestamp: <unix timestamp in seconds>
署名アルゴリズム: 署名対象のメッセージは {unix_timestamp}.{raw_json_body} だ — Unix タイムスタンプ(秒)、ドット、HTTP ボディとして送信される JSON バイトそのもの。
Signature = sha256= + hex( HMAC-SHA256( secret, "{timestamp}.{rawBody}" ) )
rawBodyワイヤー上で送信される正確なバイトでなければなりません。実装では HTTP ボディと同じシリアライゼーションで署名しなければなりません — 再シリアライズまたは再フォーマットされたバージョンへの署名は検証失敗を引き起こす。 パブリッシャー実装(署名):
import { createHmac } from 'crypto';

function signWebhook(rawBody: string, secret: string): { signature: string; timestamp: string } {
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const message = `${timestamp}.${rawBody}`;
  const hex = createHmac('sha256', secret).update(message).digest('hex');
  return {
    signature: `sha256=${hex}`,
    timestamp,
  };
}
受信側実装(検証):
import { createHmac, timingSafeEqual } from 'crypto';

function verifyWebhook(
  rawBody: string,
  signature: string,
  timestamp: string,
  secret: string,
): boolean {
  const ts = parseInt(timestamp, 10);
  if (isNaN(ts)) return false;

  // Reject requests older than 5 minutes (replay attack prevention)
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - ts) > 300) return false;

  const message = `${ts}.${rawBody}`;
  const expected = `sha256=${createHmac('sha256', secret).update(message).digest('hex')}`;

  if (signature.length !== expected.length) return false;
  return timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

app.post('/webhooks/adcp', (req, res) => {
  const sig = req.headers['x-adcp-signature'] as string;
  const timestamp = req.headers['x-adcp-timestamp'] as string;
  if (!sig || !timestamp || !verifyWebhook(req.rawBody, sig, timestamp, process.env.ADCP_WEBHOOK_SECRET)) {
    return res.status(401).end();
  }
  processWebhook(req.body);
  res.status(200).end();
});
:::caution 重要: 生ボディによる検証 署名は常に生の HTTP ボディバイトに対して検証し、パース済み JSON を再シリアライズしたバージョンに対して行ってはなりません。言語やライブラリによって JSON シリアライゼーション(キーの順序、空白、数値フォーマット)が異なる場合があります。JSON パース前に生ボディをキャプチャし、HMAC 検証にはその正確なバイトを使用すること。 Express では express.json()verify コールバックを使用して生ボディをキャプチャできる:
app.use(express.json({
  verify: (req, _res, buf) => {
    (req as any).rawBody = buf.toString('utf-8');
  },
}));
::: :::note リプレイ保護 5 分のタイムスタンプウィンドウにより、リプレイ攻撃を防ぐ。パブリッシャーは X-ADCP-Timestamp ヘッダーに Unix タイムスタンプ(秒単位、ISO 8601 ではない)を使用しなければなりません。受信側は |current_time - timestamp| > 300 秒のリクエストを拒否すべきです。 :::

信頼性

Webhook は少なくとも1回の配信を使用する — 同じイベントが複数回受信される場合があり、イベントが順序通りに届かない場合もあります。 これに対処するために task_id + status + timestamp を使用します:
async function processWebhook(payload) {
  const { task_id, status, timestamp, result } = payload;

  const task = await db.getTask(task_id);

  // 既に新しいステータスを処理済みの場合はスキップ
  if (task?.updated_at >= timestamp) return;

  await db.updateTask(task_id, { status, updated_at: timestamp, result });
  await triggerBusinessLogic(task_id, status);
}
常にポーリングをバックアップとして実装すること。 Webhook はネットワーク障害やサーバーダウンで失敗することがあります。Webhook が設定されている場合は遅いポーリング間隔(例: 30 秒ではなく 2 分ごと)を使用し、Webhook で終端ステータスを受信したらポーリングを停止します。

ベストプラクティス

  1. 常にポーリングをバックアップとして実装する — Webhook は失敗することがあります。Webhook が設定されている場合は間隔を減らして(例: 2 分ごと)ポーリングし、終端ステータスを受信したら停止します
  2. 重複を処理するtask_id + timestamp を使用して、既に処理済みまたは順序外のイベントをスキップします
  3. 署名を検証する — 処理前に常に X-ADCP-Signature を検証します
  4. すぐに応答を返す — パブリッシャーのタイムアウトや不要なリトライを避けるため、重い処理の前に 200 を返す
  5. URL 構造に依存しない — ルーティングには URL 解析ではなくペイロードの operation_id を使用します
  6. 本番環境では HMAC-SHA256 を使用する — Bearer トークンはシンプルだがペイロードの改ざんを防げない

レポート Webhook

レポート Webhook はタスクステータス Webhook とは別物です。アクティブなメディアバイの定期的なパフォーマンスデータを配信し、push_notification_config ではなく create_media_buyreporting_webhook で設定します。 詳細についてはタスクリファレンスreporting_webhook を参照。

次のステップ