> ## Documentation Index
> Fetch the complete documentation index at: https://adcp-docs-ja.pier1.co.jp/llms.txt
> Use this file to discover all available pages before exploring further.

# Push Notifications

> AdCP プッシュ通知: セラーが RFC 9421 署名付き POST リクエスト（レガシー HMAC フォールバック付き）を通じて、非同期タスクのステータス更新を Webhook エンドポイントに配信する方法。セットアップ、URL テンプレート、冪等性。

プッシュ通知により、セラーはポーリングを要求する代わりに、タスクステータスの更新をあなたに直接配信できます。タスクリクエストで Webhook URL を提供すると、タスクの進行に伴ってセラーがその URL にステータス変更を POST します。

## 仕組み

1. Webhook 相関のため、タスク呼び出しごとに一意のオペレーション ID が生成される
2. あなたのレシーバー向けに Webhook URL が構築される。URL は自身のルーティングトークンを含んでよいが、セラーにとっては不透明である
3. `push_notification_config` が URL と明示的な `operation_id` を伴ってタスクリクエストボディに注入される — 共有シークレットは不要
4. タスクステータスが変わるとセラーがあなたの URL に Webhook 通知を POST する。各 POST は、自身の brand.json の `agents[]` エントリで公開した `adcp_use: "request-signing"` 鍵で署名される。非推奨の `webhook-signing` 鍵は互換期間中も受け入れられる
5. あなたはセラーが公開する JWKS に対して署名を検証し、`idempotency_key` で重複排除する
6. 各通知はペイロードで明示的な `operation_id` をエコーバックするため、URL を解析せずに相関できる

```
create_media_buy request
  └── push_notification_config
        └── url: "https://you.com/adcp/webhook/create_media_buy/agent_123/route_abc123"
        └── operation_id: "op_456"
              // No shared secret — the seller signs with its own key, you verify against
              // its published JWKS. See "Signature verification" below.

              ↓ seller processes task ↓

POST https://you.com/adcp/webhook/create_media_buy/agent_123/route_abc123
  Signature-Input: sig1=("@method" "@target-uri" "@authority" "content-type" "content-digest");
                   created=1706097600;expires=1706097900;nonce="...";keyid="seller-webhook-2025";
                   alg="ed25519";tag="adcp/webhook-signing/v1"
  Signature: sig1=:<base64url-unpadded>:
  Content-Digest: sha-256=:<base64url-unpadded>:
  Content-Type: application/json

  {
    "idempotency_key": "whk_01HW9D3H8FZP2N6R8T0V4X6Z9B",   ← dedup by this
    "task_id": "task_456",
    "operation_id": "op_456",   ← echoed from push_notification_config.operation_id
    "status": "completed",
    "result": { ... }
  }
```

`@adcp/sdk` ライブラリを使用している場合、このフロー全体が自動的に処理されます。**バイヤー**としては、クライアントに `webhookUrlTemplate` と自身のエージェント URL を設定します。`push_notification_config` がすべての送信タスク呼び出しに注入され、受信 Webhook はセラーの JWKS に対して自動的に検証されます。**Webhook を発行するセラー**としては、brand.json の `agents[]` エントリに署名 JWK を公開します。新しい署名者は `adcp_use: "request-signing"` を使用します。Webhook 専用の鍵素材が欲しい場合は、別個の `kid` を持つ 2 つ目の `request-signing` JWK を公開します。

:::warning レガシー HMAC フォールバック（非推奨）
RFC 9421 Webhook プロファイルをまだ採用していないレシーバーと統合するバイヤーは、`push_notification_config.authentication.credentials` を設定することでレガシー HMAC-SHA256 スキームにオプトインしてもよい（MAY）。そのパスは非推奨であり AdCP 4.0 で削除されます — 下記の [レガシー HMAC-SHA256 フォールバック](#legacy-hmac-sha256-fallback-deprecated) を参照してください。Webhook を登録するインバウンドリクエストは 3.0 では通常 9421 署名されないため、`authentication` ブロックは経路上の除去/注入を受けやすい — 運用上の緩和策は [ダウングレードと注入への耐性](/docs/building/by-layer/L1/security#webhook-callbacks)を参照してください。
:::

## 命名: snake\_case vs camelCase

これは人を混乱させます。2 つの命名規則が関係しています:

| コンテキスト                       | フィールド名                     | 例                                                                            |
| ---------------------------- | -------------------------- | ---------------------------------------------------------------------------- |
| **MCP タスク引数**（AdCP JSON）     | `push_notification_config` | `{ push_notification_config: { url: ..., operation_id: ... } }`              |
| **A2A configuration オブジェクト** | `pushNotificationConfig`   | `configuration: { pushNotificationConfig: { url: ..., operation_id: ... } }` |

AdCP のフィールド名は常に **`push_notification_config`**（snake\_case）です。他のタスクパラメーターと並んでタスクリクエストボディに入ります。

A2A では、A2A プロトコルが camelCase を使う `configuration` エンベロープでこれをラップします — が、オブジェクトの中身は同一です。

## リクエストへの push\_notification\_config の追加

### MCP

`push_notification_config` をタスク引数として、他のタスクパラメーターとマージして含めます:

```json theme={null}
{
  "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/route_abc123",
    "operation_id": "op_abc123"
  }
}
```

`authentication` はデフォルトケースでは省略されます — セラーは自身の `adcp_use: "request-signing"` 鍵で署名します。非推奨の `webhook-signing` 鍵は互換期間中も受け入れられます。レガシー HMAC-SHA256 フォールバックが必要な場合のみ `authentication.credentials` を含めます。

### A2A

A2A では、スキルパラメーターは `message.parts[].data.parameters` に残ります。プッシュ通知設定はトップレベルの `configuration` オブジェクトに入ります:

```json theme={null}
{
  "message": {
    "parts": [{
      "kind": "data",
      "data": {
        "skill": "create_media_buy",
        "parameters": {
          "packages": [...]
        }
      }
    }]
  },
  "configuration": {
    "pushNotificationConfig": {
      "url": "https://you.com/webhooks/adcp/create_media_buy/route_abc123",
      "operation_id": "op_abc123"
    }
  }
}
```

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

オペレーション ID により、受信 Webhook を正しいタスク呼び出しに相関できます。パターン:

1. バイヤーがタスク呼び出しごとに一意のオペレーション ID を生成する
2. バイヤーがそれを `push_notification_config.operation_id` としてセラーに引き渡す。Webhook URL の構造はバイヤーの選択であり、**セラーにとって不透明**である
3. セラーがすべての Webhook ペイロードで `operation_id` をそのままエコーする — URL 解析は不要

**規範的なワイヤー契約:**

* **バイヤーは（SHOULD）** すべての Webhook 登録についてセラーに `operation_id` を供給し、タスク呼び出しごとに一意の値を生成すべきです（UUID 推奨）。セラーは `operation_id` を省略した Webhook 登録を `INVALID_REQUEST` で拒否してもよい（MAY）。
* **セラーは（MUST）** すべての Webhook ペイロードで、バイヤーが供給した `operation_id` 値を受け取ったとおり正確にエコーしなければなりません。ペイロードフィールドが相関の**唯一の**真実の源です。
* **セラーは（MUST NOT）** `push_notification_config.url` を解析して `operation_id` を導出してはなりません — URL 構造（パステンプレート、クエリパラメーター、不透明トークンなど）はセラーの視点からは実装依存であり、実装をまたいで確実に逆算できません。バイヤーの URL 規約はプロトコルの一部ではありません。
* **レシーバーは（MAY）** URL パスまたはクエリ文字列で HTTP エンドポイントをディスパッチしてもよいが、URL 由来の値をオペレーション相関キーとして使用してはなりません（MUST NOT）。ワイヤーレベルの相関識別子はペイロードフィールドです。

これは、アドテックにおけるすべての比較可能な非同期通知プロトコル（OpenRTB の `nurl`/`burl`、VAST トラッキングピクセル、A2A の `PushNotificationConfig`）が示す先例と一致します: HTTP 呼び出しを発火するエンティティは、相関データのためにレシーバーの URL を決して解析しません。

**URL テンプレートパターン（バイヤー側の規約のみ）:**

```
https://you.com/webhooks/{task_type}/{agent_id}/{route_token}
```

上記のテンプレートは、有用な**バイヤー向けのサーバー側ルーティング補助**です — バイヤーの HTTP サーバーがボディを先に解析せずにパスセグメントでディスパッチできるようにします — が、規範的ではなく、セラーはそれに依存できません。`?route=…`、フラットなパス、または完全に不透明なトークンを好むバイヤーも、セラー側の `operation_id` が SDK の送信側 API を通じて供給される限り、完全にコンフォーマントです。

**例（クライアントライブラリが自動処理）:**

```typescript theme={null}
import { randomUUID } from 'crypto';

const operationId = randomUUID(); // e.g. "cd51e063-2b79-4a6d-afac-ed7789c3a443"
const routeId = randomUUID(); // receiver-local routing token; opaque to the seller
const webhookUrl = `https://you.com/adcp/webhook/create_media_buy/${agentId}/${routeId}`;

// pass both webhookUrl and operationId in push_notification_config
```

セラーの Webhook ペイロードには `"operation_id": "cd51e063-2b79-4a6d-afac-ed7789c3a443"` が含まれるため、ハンドラーはペイロードフィールドを直接読むことで正しい保留中のオペレーションに相関できます。URL パスは HTTP ハンドラーを選択できますが、プロトコルのオペレーション ID は選択できません。

**セラー SDK 実装**は、送信側 Webhook API の明示的なパラメーターとして `operation_id` を公開します（例: Python `WebhookSender.send_mcp(url=…, operation_id=…)`）。セラーのアプリケーションコードが元のタスクリクエストから Webhook 発火へ値を引き通します。SDK は URL からそれを回復しようとは決してしません。

### 呼び出し元の `context` オブジェクトのエコー

発信元のリクエストがトップレベルの `context` オブジェクトを運んだ場合、セラーは同じオペレーションのすべての Webhook ペイロードで、`operation_id` と並んでその同じオブジェクトをそのままエコーしなければなりません（MUST）。これは同期および非同期ステータスレスポンスに適用されるのと同じ契約です — [コンテキストとセッション — 規範的なエコー契約](/docs/building/by-layer/L2/context-sessions#normative-echo-contract)を参照してください。エコーは `working`、`input-required`、`completed`、`failed`、`canceled` の配信を通じて引き継がれなければなりません（MUST）。初回レスポンスと後の Webhook の間で `context` を落とすと、まさに最も必要とされる箇所でバイヤー側の相関が壊れます。`context.trace_id` または `context.internal_campaign_id` でルーティングするバイヤーは、すべての配信でのそのままのエコーに依存します。

## Webhook が発火するとき

Webhook は、`push_notification_config` がリクエストにある限り、初回レスポンス後の各ステータス変更ごとに送信されます。

タスクが同期的に完了する場合（初回レスポンスがすでに `completed`、`failed`、`rejected` などの終端状態）、Webhook は送信されません — すでに結果を持っているからです。

初回レスポンスが非終端（`working` または `submitted`）であるオペレーションのみが、後で AdCP タスク Webhook を発行できます。インラインの終端レスポンスに対して、セラーは `task_id` を捏造したりインライン結果を `push_notification_config.url` にリプレイしたりしてタスク Webhook を合成してはなりません（MUST NOT）。同期専用オペレーション、および `push_notification_config` が意味を持たない同期専用オペレーションモードは、代わりにそのフィールドを整形式のランタイムエラーとして拒否してもよい（MAY）。これはセラー対バイヤーのワイヤールールです: バイヤー SDK は同期レスポンスをローカルのコールバック、Promise、ハンドラー呼び出しに正規化してもよい（MAY）が、それらのローカル SDK の便宜は AdCP Webhook ではありません。将来の AdCP バージョンがバイヤー通知の同期完了通知モードを追加する場合、それは明示的でケイパビリティ宣伝されます。3.x のタスク Webhook 契約はそれを定義しません。

**Webhook をトリガーするステータス変更:**

| ステータス            | 意味                    |
| ---------------- | --------------------- |
| `working`        | タスク処理中 — 進捗情報を含むことがある |
| `input-required` | 人間の承認または明確化を待機中       |
| `completed`      | 最終結果が利用可能             |
| `failed`         | タスクがエラー詳細付きで失敗        |
| `canceled`       | タスクがキャンセルされた          |

## Webhook ペイロード形式

### MCP

```json theme={null}
{
  "idempotency_key": "whk_01HW9D3H8FZP2N6R8T0V4X6Z9B",
  "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" } }
    ]
  }
}
```

すべての Webhook ペイロードは必須の `idempotency_key` を運びます — 同じイベントのリトライをまたいで安定する、送信者生成の鍵です。これは正準の重複排除フィールドです。下記の [信頼性](#reliability) を参照してください。

### ペイロードの構造: エンベロープ vs. result

Webhook レシーバーは、**ワイヤーエンベロープ**とタスク固有の **result** を区別しなければなりません。完全な MCP Webhook エンベロープは、HTTP POST ボディとして送信される JSON オブジェクトです。デリバリーレポートの内容は `result` の下に存在します。それ自体はトップレベルの POST ボディとしては有効ではありません。

デリバリーレポートの発火では、完全なワイヤーペイロードは次のようになります:

```json theme={null}
{
  "idempotency_key": "whk_20260526_example_000031",
  "operation_id": "delivery_report_67_2026_04",
  "task_id": "delivery_report_67_2026_04_000031",
  "task_type": "media_buy_delivery",
  "status": "completed",
  "timestamp": "2026-05-26T09:00:44.582Z",
  "message": "Scheduled media buy delivery report available",
  "result": {
    "notification_type": "scheduled",
    "sequence_number": 31,
    "reporting_period": {
      "start": "2026-05-25T00:00:00Z",
      "end": "2026-05-25T23:59:00Z"
    },
    "currency": "USD",
    "media_buy_deliveries": [
      {
        "media_buy_id": "mb_001",
        "status": "active",
        "totals": {
          "impressions": 125000,
          "spend": 5625.0,
          "clicks": 250
        },
        "by_package": []
      }
    ]
  }
}
```

この内側の result オブジェクトは有効なデリバリーレポートの内容ですが、**トップレベルの Webhook POST ボディとしては有効ではありません**:

```json theme={null}
{
  "notification_type": "scheduled",
  "sequence_number": 31,
  "reporting_period": {
    "start": "2026-05-25T00:00:00Z",
    "end": "2026-05-25T23:59:00Z"
  },
  "currency": "USD",
  "media_buy_deliveries": []
}
```

トップレベルの `status` は非同期 Webhook ステータス（`completed`、`failed`、`working` など）です。ネストされた `media_buy_deliveries[].status` はメディアバイのライフサイクルまたはレポート状態（`active`、`paused`、`reporting_delayed` など）です。2 つのフィールドを混同しないでください。

`scheduled`、`final`、`delayed`、`adjusted` などのデリバリーレポートデータイベントでは、`notification_id` は設計上不在です。トランスポートイベントは `idempotency_key` で重複排除してください。`aggregated_totals` フィールドは `get_media_buy_delivery` レスポンス専用の API 専用であり、レポート Webhook の result ペイロードで発行してはなりません。

署名とコンテンツダイジェストは、この完全なエンベロープとして送信される正確な生の JSON バイト上で計算されます。検証前に `result` オブジェクトだけを再シリアライズしたり、空白を追加したり、フィールド順を変えたりすると、バイトが変わり署名検証が壊れます。

### A2A

A2A は `Task` オブジェクト（最終状態向け）または `TaskStatusUpdateEvent`（進捗向け）を送信します。最終状態（`completed`、`failed`）では、AdCP result データは `.artifacts[0].parts[]` にあります。中間状態（`working`、`input-required`）では、データは `status.message.parts[]` にあります。

```json theme={null}
{
  "id": "task_456",
  "contextId": "ctx_123",
  "status": {
    "state": "completed",
    "timestamp": "2025-01-22T10:30:00Z"
  },
  "artifacts": [{
    "artifactId": "result",
    "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" } }
          ]
        }
      }
    ]
  }]
}
```

### プロトコル比較

|                  | MCP                                | A2A                                                                 |
| ---------------- | ---------------------------------- | ------------------------------------------------------------------- |
| **Config フィールド** | `push_notification_config`（タスク引数内） | `configuration.pushNotificationConfig`（スキルパラメーターと別）                 |
| **エンベロープ**       | `mcp-webhook-payload.json`         | ネイティブ `Task` / `TaskStatusUpdateEvent`                              |
| **Result の場所**   | `result` フィールド                     | `.artifacts[0].parts[].data`（最終）/ `status.message.parts[].data`（中間） |
| **データスキーマ**      | 同一の AdCP スキーマ                      | 同一の AdCP スキーマ                                                       |

### 登録チャネルがエンベロープ形状を決定する

Webhook エンベロープの形状は、同期リクエストがどのトランスポートで送られたかではなく、**バイヤーがどの登録メカニズムを使ったか**で決まります:

| 登録経路                                                                                                                                                                                 | 配信されるエンベロープ                                                   |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------- |
| AdCP `push_notification_config`（タスク引数、MCP/A2A/REST）                                                                                                                                  | [`mcp-webhook-payload.json`](#mcp)                            |
| A2A `TaskPushNotificationConfig`（[`CreateTaskPushNotificationConfig`](https://a2a-protocol.org/latest/specification/) RPC、または `SendMessage` 上のインライン `task_push_notification_config`） | A2A 1.0 §4.3.3 に従う A2A ネイティブ `Task` / `TaskStatusUpdateEvent` |

2 つのチャネルは独立しています。バイヤーは同じタスクについて両方を登録し、ステータス変更ごとに両方の Webhook を受け取ってもよい（MAY）。

**なぜこれがモデルであり「インバウンドトランスポートに合わせる」ではないのか。** 各チャネルはそのエンベロープ専用に作られています: AdCP `push_notification_config` は AdCP `mcp-webhook-payload` 形状のための AdCP 層の登録、A2A `TaskPushNotificationConfig` は A2A 自身の `StreamResponse` ラップ配信のための A2A 層の登録です。バイヤーはレシーバーに合うチャネルを選びます — 判別子フィールドは不要で、オーバーライドすべき曖昧さもありません。

**典型例: A2A で同期、AdCP 形状の Webhook。** MCP ネイティブのランタイムからオーケストレーションし、1 つの特定の高スループット同期オペレーションに A2A を使うバイヤーは、その `SendMessage` ボディ内の AdCP タスク引数に `push_notification_config` を入れます。セラーは、同期トランスポートが A2A であることに関わらず、それを AdCP 形状の登録として尊重します。バイヤーのレシーバーは、パイプライン内の他のすべての AdCP Webhook と同じ `mcp-webhook-payload` 形状を受け取ります。

**A2A 形状の Webhook を望む A2A バイヤー**は、A2A のネイティブプッシュ通知メカニズムを通じて登録します。AdCP はそのケースのために何も追加する必要はありません。

### ステータス別の result データ

| ステータス                  | `result` / `data` の内容                         |
| ---------------------- | --------------------------------------------- |
| `completed` / `failed` | 完全なタスクレスポンス                                   |
| `working`              | 進捗: `percentage`、`current_step`、`total_steps` |
| `input-required`       | 理由と検証エラー                                      |
| `submitted`            | 最小限の確認                                        |

## 署名検証

すべての AdCP 3.0 Webhook は [RFC 9421 Webhook プロファイル](/docs/building/by-layer/L1/security#webhook-callbacks)の下で署名されます。セラーは、自身の brand.json の `agents[]` エントリで公開した `adcp_use: "request-signing"` 鍵で署名します。非推奨の `webhook-signing` 鍵は互換期間中も受け入れられます。あなたはセラーが公開する JWKS に対して検証します。共有シークレットはワイヤーを渡りません。

**パブリッシャーは 3 つのヘッダーを送ります**（`Content-Type` に加えて）:

```
Signature-Input: sig1=("@method" "@target-uri" "@authority" "content-type" "content-digest");
                 created=<unix>;expires=<unix>;nonce=<base64url>;
                 keyid=<kid>;alg="ed25519";tag="adcp/webhook-signing/v1"
Signature: sig1=:<base64url-unpadded>:
Content-Digest: sha-256=:<base64url-unpadded>:
```

カバーされるコンポーネントは固定です: `@method`、`@target-uri`、`@authority`、`content-type`、`content-digest`。`content-digest` は REQUIRED です — ボディがイベントそのものであり、それをカバーしない署名は重要な攻撃面を保護していません。

**検証**は、14 ステップの[リクエスト検証器チェックリスト](/docs/building/by-layer/L1/security#verifier-checklist-requests)に、3 つの Webhook 置換を加えたものに従います:

* エラーコードは `webhook_signature_*` プレフィックスを使う（[Webhook エラータクソノミー](/docs/building/by-layer/L1/security#webhook-error-taxonomy)を参照）。
* `tag` は `adcp/webhook-signing/v1` でなければならない（MUST）。
* `keyid` はセラーオペレーターの `brand.json` の `agents[].jwks_uri` を介して解決し、存在する場合はパブリッシャーの `adagents.json` の `signing_keys[]` ピンを適用する（統合からセラーのエージェント URL はすでに持っている）。

**レシーバー実装スケッチ:**

```typescript theme={null}
import { createRemoteJWKSet, jwtVerify } from 'jose';
// Use a validated RFC 9421 library (e.g., `http-message-signatures`) pinned to the AdCP profile.

app.post('/webhooks/adcp/*', async (req, res) => {
  try {
    // 1. Parse Signature-Input / Signature headers and reject on malformed.
    // 2. Resolve keyid against the seller operator's brand.json JWKS.
    // 3. Run the AdCP webhook verifier checklist (14 steps).
    await verifyAdcpWebhookSignature(req, {
      sellerAgentUrl: req.sellerContext.agentUrl, // known from your integration
      requiredTag: 'adcp/webhook-signing/v1',
      allowedAlgs: ['ed25519', 'ecdsa-p256-sha256'],
    });
  } catch (err) {
    return res.status(401)
      .setHeader('WWW-Authenticate', `Signature error="${err.code}"`)
      .end();
  }

  // 4. Dedup by idempotency_key before applying side effects (see Reliability below).
  processWebhook(req.body);
  res.status(200).end();
});
```

:::caution 生のボディと content-digest
`Content-Digest` の検証（チェックリストのステップ 11）には、生の HTTP ボディバイトが必要です。JSON パースの前にそれらをキャプチャしてください — いかなる再シリアライズもダイジェストの一致を壊します。

Express では:

```typescript theme={null}
app.use(express.json({
  verify: (req, _res, buf) => { (req as any).rawBody = buf.toString('utf-8'); },
}));
```

:::

:::note リプレイ保護
`created`/`expires`/`nonce` の sig-params は、5 分の最大有効ウィンドウと `(keyid, nonce)` のリプレイ重複排除を強制します。keyid ごとの上限とメモリ制限ルールは [トランスポートリプレイ重複排除](/docs/building/by-layer/L1/security#transport-replay-dedup)を参照してください。
:::

### レガシー HMAC-SHA256 フォールバック（非推奨）

:::warning 非推奨 — AdCP 4.0 で削除
下記の HMAC-SHA256 スキームは 3.x のみの互換性のための便宜です。新しい統合は `push_notification_config.authentication` を省略し、上記の [9421 Webhook プロファイル](#signature-verification)を使用すべきです（SHOULD）。セラーはレガシースキームのサポートを断ってもよい（MAY）。
:::

バイヤーは `push_notification_config.authentication.credentials` を設定することで HMAC-SHA256 にオプトインできます。設定されている場合、セラーは共有シークレットを使って HMAC-SHA256 で署名し、リプレイ保護のためにタイムスタンプを含めます。

**設定（レガシー）:**

```json theme={null}
{
  "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` はワイヤー上で送信される正確なバイトで**なければなりません**。JSON ペイロードをシリアライズしてボディを生成する際は、**コンパクトなセパレーター**（`","` と `":"`、周囲の空白なし）を使用してください — これは JavaScript の `JSON.stringify` とほとんどの HTTP クライアントのデフォルトに一致し、レシーバーが `raw_body` として見るものです。ここでのよくあるクロス SDK の失敗は、署名者が空白を挿入する言語デフォルト（例: Python `json.dumps(payload)`）を呼ぶ一方、HTTP クライアントがコンパクトなバイトをワイヤーに書き込むケースです — 署名者はレシーバーが決して見ないバイト上で署名します。バイト等価性のために `json.dumps(payload, separators=(",", ":"))`（または同等物）を使用してください。正準のワイヤー形式と検証器入力の扱いに関する完全なルールは [Webhook セキュリティ — レガシー規範ルール](/docs/building/by-layer/L1/security#legacy-hmac-sha256-fallback-deprecated-removed-in-40)を参照してください。

**パブリッシャー実装（レガシー）:**

```typescript theme={null}
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 };
}
```

**レシーバー実装（レガシー）:**

```typescript theme={null}
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;
  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));
}
```

レガシースキームの規範ルールは [Webhook セキュリティ](/docs/building/by-layer/L1/security#legacy-hmac-sha256-fallback-deprecated-removed-in-40)にあります。

### レガシー Bearer トークン（非推奨）

A2A の `authentication.schemes: ["Bearer"]` スキームも互換性のためにサポートされ、AdCP 4.0 で削除されます。Bearer はボディに対する改ざん保護を提供しません。9421 プロファイルは署名者アイデンティティ（JWKS アンカー、ローテーション可能、失効可能）と鍵管理（ワイヤー上に共有シークレットなし）でより強力です。ボディ完全性の保護は、両者ともボディバイトをカバーするためレガシー HMAC スキームと同等です。セラーは変更系コールバックについて Bearer を拒否すべきです（SHOULD）。

```json theme={null}
{
  "authentication": {
    "schemes": ["Bearer"],
    "credentials": "your_bearer_token_min_32_chars"
  }
}
```

```javascript theme={null}
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();
});
```

## 信頼性

Webhook は**少なくとも 1 回の配信**を使用します — 同じイベントを複数回受け取ることがあり、イベントは順不同で到着することがあります。

### `idempotency_key` による重複排除

すべての Webhook ペイロード — MCP タスクエンベロープ、ガバナンスのリスト変更 Webhook（`collection_list_changed`、`property_list_changed`）、アーティファクトプッシュ Webhook、権利の `revocation-notification` — は必須の `idempotency_key` を運びます。パブリッシャーはこの鍵を個別のイベントごとに 1 回生成し、すべてのリトライで再利用します。レシーバーはそれで重複排除しなければなりません（MUST）。

**送信者の要件:**

* 鍵は暗号学的にランダムでなければなりません（MUST、UUID v4 推奨）。連番、タイムスタンプのみ、その他の予測可能な値は非コンフォーマントです: レシーバーは生の値で重複排除するため、予測可能な鍵は攻撃者がレシーバーのキャッシュに事前投入して後の正当なイベントを抑制することを可能にします。
* 鍵は同じイベントのリトライをまたいで安定でなければならず（MUST）、個別のイベントに再利用してはなりません（MUST NOT）。

**レシーバーの要件:**

* 重複排除のスコープは `(認証された送信者アイデンティティ, idempotency_key)` です。「認証された送信者アイデンティティ」とは、署名検証によって確立された送信者の暗号学的アイデンティティを意味します — 9421 デフォルトでは、解決された `keyid` → 署名者の `agents[]` エントリ URL、レガシーフォールバックでは、検証された HMAC シークレットまたは Bearer トークンからのクレデンシャルバインディング。アイデンティティをペイロードフィールドから導出してはなりません。異なる送信者からの鍵は独立したキースペースに保たなければならず（MUST）、複数のセラーと統合するレシーバーはそれらを統合してはなりません（MUST NOT）。HMAC→9421 移行中は、レシーバーは同じ論理セラーの両方の送信者アイデンティティ形式を 1 つのキースペースにマップし、スキームをまたぐ重複も重複排除されるようにすべきです（SHOULD）。
* **クロスエンドポイント重複排除（MUST）。** 複数の Webhook エンドポイント（統合ごと、環境ごと、テナントごと、または水平スケールされたフリートのポッドごと）を公開するレシーバーは、ある送信者が到達できるすべてのエンドポイントにわたって `(送信者アイデンティティ, idempotency_key)` キースペースを共有しなければなりません（MUST） — ポッドごとのインメモリキャッシュは非コンフォーマントです。共有ティアがなければ、同じ署名済みイベントが兄弟エンドポイントにリプレイされると 2 回実行されます。`(keyid, nonce)` スコープに関するトランスポート層の対応ルールは [Webhook リプレイ重複排除のサイジング](/docs/building/by-layer/L1/security#webhook-replay-dedup-sizing)を参照してください。
* 重複排除の状態は、プロセス再起動、ポッド置換、リージョンフェイルオーバーを生き延びる耐久ストレージに少なくとも 24 時間永続化しなければなりません（MUST）。パブリッシャーはそのウィンドウを超えてリトライすべきではありません（SHOULD NOT）。レシーバーの TTL 後に到着するリトライは新しいイベントとして再処理されます。インメモリのみのキャッシュ（バッキングティアなしのポッドごとの `Map` または LRU）は非コンフォーマントです — 約 360 秒の署名ノンスウィンドウと 24 時間の冪等性ウィンドウの非対称性が、**変位リプレイウィンドウ**を作ります。そこでは、正当な署名済みリトライ（新しいノンス、同じ `idempotency_key`）が署名検証を通過し、レシーバーがインメモリ状態を落としたためキャッシュエントリを見つけられません。副作用が 2 回実行されます。キャッシュティアが 24 時間を耐久的に守れないレシーバーは、統合するすべての送信者に、より短い実効ウィンドウを文書化しなければなりません（MUST） — 黙って短縮するのが危険なモードです。
* レシーバーは送信者ごとに重複排除キャッシュサイズを制限し、無制限に成長させるのではなく `429 Too Many Requests` を返す（または接続を切る）べきです（SHOULD） — 高ボリュームの新しい鍵を発行する誤動作または敵対的なセラーは、さもなくばストレージ増幅のベクトルになります。
* **重複は `2xx`（通常 `200 OK`）で応答しなければならず（MUST）**、`409 Conflict` ではありません。少なくとも 1 回の送信者は、2xx 以外のレスポンスを「配信失敗」と解釈し、指数バックオフでリトライします。正常に重複排除されたイベントに `4xx` を返すと、正しいレシーバーの挙動がリトライ嵐に変わります。重複はエラーではなく no-op です。
* Webhook レシーバーは、鍵再利用をまたいだペイロード等価性を検証**しません**。送信者が変更されたペイロードで鍵を再利用した場合（送信者のバグ）、レシーバーのキャッシュされた最初のコピーが勝ち、2 つ目は黙って重複排除されます。これはリクエスト側の `IDEMPOTENCY_CONFLICT` の挙動とは異なります — 送信者は個別のイベントごとに新しい鍵を生成することについて単独で責任を負います。

```javascript theme={null}
app.post('/webhooks/adcp', async (req, res) => {
  const payload = req.body;
  const { idempotency_key, task_id, status, timestamp, result } = payload;

  // Scope dedup to the authenticated sender — never trust a payload field for identity.
  const sender = req.verifiedSenderId; // set by 9421 verifier (keyid → agent URL) or legacy HMAC/Bearer middleware

  // Dedup: same (sender, idempotency_key) within the replay window → already processed.
  // Return 200 (not 409) so the sender stops retrying.
  if (await db.webhookAlreadyProcessed(sender, idempotency_key)) {
    return res.status(200).end();
  }
  await db.markWebhookProcessed(sender, idempotency_key); // before side effects — fail-closed on crash

  // Ordering: separately, don't apply a stale status on top of a newer one.
  // Ordering state is keyed on task_id, not idempotency_key — two distinct events
  // (different keys) can still arrive out of order. Still a 200: we received it cleanly.
  const task = await db.getTask(task_id);
  if (task?.updated_at >= timestamp) {
    return res.status(200).end();
  }

  await db.updateTask(task_id, { status, updated_at: timestamp, result });
  await triggerBusinessLogic(task_id, status);
  res.status(200).end();
});
```

**必ずバックアップとしてポーリングを実装してください。** Webhook はネットワークの問題やサーバーダウンで失敗することがあります。Webhook が設定されている場合はより遅いポーリング間隔（例: 30 秒ではなく 2 分ごと）を使い、Webhook で終端ステータスを受け取ったらポーリングを停止します。

### 発火漏れの診断

バイヤーが Webhook がエンドポイントに届いていないと疑う場合 — ゲートウェイの 5xx、古いシーケンスの重複排除、ドリフトした Webhook URL、作動したサーキットブレーカー下での発火抑制 — [`get_media_buys`](/docs/media-buy/task-reference/get_media_buys#webhook-activity) を `include_webhook_activity: true` で呼び出します。返される各メディアバイは、呼び出し元プリンシパルの最近の発火の `webhook_activity` 配列を運びます。これには `idempotency_key`（ペイロードの重複排除キーと一致 — 自身のエンドポイントログと照合）、`status`（`success` / `failed` / `timeout` / `connection_error` / `pending`）、`http_status_code`、`attempt`、`error_message` が含まれます。スコープは呼び出し元プリンシパル自身の発火です。オペレーターチケットは不要です。

## ベストプラクティス

1. **必ずバックアップとしてポーリングを実装する** — Webhook は失敗し得る。Webhook が設定されている場合は間隔を減らして（例: 2 分ごと）ポーリングし、終端ステータスを受け取ったら停止する
2. **`idempotency_key` で重複排除する** — すべてのペイロードはリトライをまたいで安定する必須の鍵を運ぶ。処理済みの鍵を少なくとも 24 時間追跡する
3. **重複には 2xx を返す** — 正常に重複排除されたイベントはエラーではなく no-op。2xx 以外を返すと送信者のリトライバックオフをトリガーしリトライ嵐を作る
4. **処理前に署名を検証する** — いかなる副作用の前にも 9421 Webhook 検証器チェックリスト（またはオプトインした場合はレガシー HMAC チェック）を実行する
5. **即座に確認応答する** — セラーのタイムアウトと不要なリトライを避けるため、重い処理の前に `200` を返す
6. **URL 構造に依存しない** — ビジネス相関にはペイロードの `operation_id` を使う。URL パスはエンドポイントの多重分離のみに使ってよい
7. **4.0 での HMAC 削除に備える** — 現在レガシー HMAC フォールバックを使っている場合、3.x の間に 9421 Webhook プロファイルへ移行する

## ペイロード抽出

Webhook レシーバーは形式を検出し AdCP データを抽出する必要があります。バイヤーはトランスポートを設定したため通常は形式を知っていますが、防御的な検出はマルチフォーマットレシーバーに有用です。

### 形式検出

| シグナル                          | 形式  |
| ----------------------------- | --- |
| `status` が文字列、`task_id` が存在   | MCP |
| `status` が `.state` を持つオブジェクト | A2A |

### 抽出

**MCP Webhook:** `result` フィールドから直接データを抽出します。

**A2A Webhook:** [A2A レスポンス抽出](/docs/building/by-layer/L0/a2a-response-extraction)アルゴリズムを使用します — 最終状態は `.artifacts[0].parts[]`（最後の DataPart）から、中間状態は `status.message.parts[]`（最初の DataPart）から抽出します。

```javascript theme={null}
function extractAdcpResponseFromWebhook(payload, knownFormat) {
  const format = knownFormat || detectFormat(payload);

  if (format === 'mcp') return payload.result ?? null;
  if (format === 'a2a') return extractAdcpResponseFromA2A(payload);
  return null;
}

function detectFormat(payload) {
  if (payload.status && typeof payload.status === 'object'
      && !Array.isArray(payload.status) && payload.status.state) return 'a2a';
  if (typeof payload.status === 'string' && payload.task_id) return 'mcp';
  return null;
}
```

### セキュリティ要件

* **Content-Type 検証**: 送信者は `application/json` を送らなければなりません（MUST）。レシーバーは署名検証の前に他のタイプを拒否しなければなりません（MUST）。
* **ペイロードサイズ制限**: レシーバーは 1MB 制限を強制すべきです（SHOULD）。署名検証の前に拒否します — 大きなペイロード上でダイジェストや HMAC を計算するのは DoS ベクトルです。`413 Payload Too Large` を返します。
* **重複排除**: `idempotency_key` が正準の重複排除フィールドです。署名検証（9421 またはレガシー HMAC）とリプレイ重複排除がトランスポートを保護し、`idempotency_key` がアプリケーション層で重複する副作用から保護します。
* **形式検出**: 自動検出は防御的なフォールバックです。レシーバーはペイロード検査のみに頼るのではなく、トランスポート設定からの既知の形式（`knownFormat` パラメーター）を使用すべきです（SHOULD）。侵害された中間者が、抽出を誤ったパスにルーティングする曖昧なペイロードを作る可能性があります。

### テストベクター

機械可読のテストベクターは [`/static/test-vectors/webhook-payload-extraction.json`](https://adcontextprotocol.org/test-vectors/webhook-payload-extraction.json) で利用できます。クライアントライブラリは、形式検出と抽出のロジックをこれらのベクターに対して検証すべきです（SHOULD）。

## レポート Webhook

レポート Webhook はタスクステータス Webhook とは別です。アクティブなメディアバイの定期的なパフォーマンスデータを配信し、`push_notification_config` ではなく `create_media_buy` の `reporting_webhook` を通じて設定されます。

`reporting_webhook` の詳細は [Task Reference](/docs/media-buy/task-reference) を参照してください。

## 永続チャネル契約

タスク Webhook は論理タスクごとに 1 回発火し、タスクが確定すると停止します。**永続 Webhook** — メディアバイ上の `reporting_webhook` と `push_notification_config` — は単一のオペレーションより長く続き、リソースの寿命の間繰り返し発火します。以下の契約は永続チャネルに適用されます。

このセクションは [スナップショットとログ契約](/docs/protocol/snapshot-and-log)のトランスポート側の半分です。読み取り側のルール（スナップショットが権威、リプレイ = 再読み取り）は、そのページを参照してください。

### 配信セマンティクス

* **少なくとも 1 回の配信。** セラーはリトライ下で同じ論理イベントを再発火してもよい（MAY）。レシーバーは `idempotency_key` でトランスポートのリトライを重複排除しなければなりません（MUST）。型付き `notification_id` も運ぶ状態形状イベント（[`mcp-webhook-payload.json`](https://adcontextprotocol.org/schemas/v3/core/mcp-webhook-payload.json) と [snapshot-and-log Rule 1](/docs/protocol/snapshot-and-log#1-two-distinct-ids-per-fire-and-per-state) を参照）については、レシーバーは発火を現在のスナップショット状態に相関させるため `notification_id` も追跡しなければなりません（MUST） — 同じ `notification_id` を 2 つの異なる `idempotency_key` 値の下で見ることは、トランスポートのリトライではなく再発行のシグナルです。
* **順序保証なし。** 同じリソース上の 2 つのイベントが数秒以内に順不同で到着してもよい（MAY）。レシーバーは Webhook の順序を正準として扱うのではなく、リソーススナップショットを通じてリコンサイルしなければなりません（MUST）。
* **冪等な適用。** 同じペイロードを 2 回適用しても、結果のレシーバー状態は同一でなければなりません（MUST）。

### 合体（Coalescence）

状態形状のイベントタイプについて、セラーは同じリソース上の複数のほぼ同時の変更を単一のプッシュに合体させるべきです（SHOULD）。**合体ウィンドウはイベントタイプごとであり、一律の上限ではありません** — レイテンシーに敏感なイベント（不正、ブランドセーフティ）は、アドバイザリと同じウィンドウを待てません。

| イベントタイプ                | デフォルト合体ウィンドウ  | 備考                                                                         |
| ---------------------- | ------------- | -------------------------------------------------------------------------- |
| `impairment`（一般）       | 5 分（超えるべきでない） | リソース状態の障害のデフォルト — オーディエンス停止、クリエイティブ失効など                                    |
| `impairment`（レイテンシー敏感） | サブ分 / 合体なし    | 不正駆動、ブランドセーフティ駆動、その他バイヤーの応答ウィンドウが短いクラス。セラーはこれらに一般デフォルトを適用してはならない（MUST NOT） |
| 将来のアドバイザリイベント          | 数時間〜日次        | ノイズ許容度が高い。より大きなウィンドウが適切                                                    |
| 将来のディフェクトイベント          | 数分〜数時間        | 緊急度は impairment とアドバイザリの間                                                  |

セラーは、デフォルト未満のレイテンシーを必要とするレシーバー向けに、`get_agent_capabilities` を通じてより短い合体ウィンドウを宣言してもよい（MAY）。セラーは、レシーバー側で宣言された明示的なバイヤーのオプトインなしに、タイプごとのデフォルトを超えてはなりません（MUST NOT）。デリバリーレポートの発火（`scheduled`、`final`）は独自のケイデンスに従い、この合体ルールの対象外です。

### リプレイと回復

バイヤーのレシーバーがオフラインで発火を逃した場合、回復は**スナップショットを読む**ことです。すべての永続チャネルに 2 つのパスが存在し、内容は同等です:

* `impairment` イベントを逃した → `get_media_buys` を呼んで `impairments[]` を読む（完全な状態回復）。
* デリバリーレポートの発火を逃した → 該当ウィンドウについて、セラーが `reporting_capabilities.windowed_pull_granularities`（#4590）で宣言した粒度に `time_granularity` を設定して `get_media_buy_delivery` を呼ぶ。プルは Webhook が配信したのと同じウィンドウごとのスライスを返す。ウィンドウ粒度をまだ宣言していないセラーは、日付範囲の集計と日次内訳のみを返し、サブ日次の発火を再構築できない。
* その他の状態形状イベントを逃した → 対応する `get_*` タスクを呼ぶ。

AdCP はトランスポート層でイベントリプレイのプリミティブにコミットしません。Webhook 配信可視化面（`get_media_buys` 上の `webhook_activity[]`、[#4278](https://github.com/adcontextprotocol/adcp/issues/4278) で提案）は、**デバッグ**のために保持ウィンドウ内の最近の発火を公開します — バイヤーはこれを使って、セラーが発火したこととレシーバーが返した HTTP ステータスを検証します。それはデータ回復チャネルではありません。それはスナップショットのウィンドウごとのプル（#4590）の役割です。

### 可変性とローテーション

メディアバイ上の `push_notification_config` と `reporting_webhook` は、バイを再作成せずに `update_media_buy` を通じて更新してもよい（MAY）。よくある理由: レシーバー URL のローテーション、期限切れ bearer トークンの置換、署名鍵のスワップ。

セラーは、更新が確認応答された後の次の発火で更新された設定を尊重しなければなりません（MUST）。正式なハンドオフウィンドウはありません — バイヤーは伝播ウィンドウ中に以前の URL に対して少数の発火を受け取ることがあり（MAY）、以前の URL が合体ウィンドウの間静かになるまで両方の URL をライブとして扱うべきです（SHOULD）。

### 認証更新

永続 Webhook は bearer トークンより長く続きます。bearer 認証（レガシー HMAC プロファイルまたはトークンベース mTLS）を使うレシーバーは、期限切れ前に `update_media_buy` を通じてトークンをローテーションすべきです（SHOULD）。9421 署名プロファイルを使うレシーバーはトークンローテーションを必要としません — 検証はセラーが公開する JWKS に対して行われ、セラーはそれを独立してローテーションします。

セラーの発火がレシーバーから 401 を受け取った場合、セラーはこれを一時的なレシーバー側の設定エラーとして扱うべきです（SHOULD）: 標準スケジュールでリトライし、デバッグのため `webhook_activity[]` に失敗を表面化し、Webhook を自動無効化しない。

### 終了

永続 Webhook はバイの終端ライフサイクル遷移を通じて発火します:

* `final` デリバリーレポートは、バイが `completed`、`canceled`、`rejected` に達した後に発火する。
* 保留中の `impairment` イベントは、セラーがキューに持っている場合、終了前に発火する（または合体されて発火する）。
* 最終発火の後、設定された URL に対してそれ以上のイベントは発火しない。セラーは、バイヤーが終了シーケンスを監査できるよう、終了後の保持ウィンドウの間 `webhook_activity[]` を保持してもよい（MAY）。

## 次のステップ

* [Task Lifecycle](/docs/building/by-layer/L3/task-lifecycle) — ステータス値と遷移
* [Async Operations](/docs/building/by-layer/L3/async-operations) — 長時間実行タスクの処理
* [Error Handling](/docs/building/by-layer/L3/error-handling) — Webhook エラーパターン
