{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://adcontextprotocol.org/schemas/3.2.0-beta.0/core/mcp-webhook-payload.json",
  "title": "MCP Webhook Payload",
  "description": "Standard envelope for AdCP application-layer HTTP push notifications. This defines the wire format sent to the URL configured in task input `push_notification_config`, including when that task was invoked through the AdCP A2A profile. It is distinct from A2A transport notifications registered through `configuration.taskPushNotificationConfig`, which use native StreamResponse branches.",
  "type": "object",
  "allOf": [
    {
      "if": {
        "properties": {
          "task_type": {
            "const": "get_products"
          },
          "result": {
            "properties": {
              "status": {
                "const": "rejected"
              }
            },
            "required": [
              "status"
            ]
          }
        },
        "required": [
          "task_type",
          "result"
        ]
      },
      "then": {
        "properties": {
          "result": {
            "$ref": "https://adcontextprotocol.org/schemas/3.2.0-beta.0/media-buy/get-products-rejected.json"
          }
        }
      }
    }
  ],
  "properties": {
    "idempotency_key": {
      "type": "string",
      "description": "Sender-generated key stable across retries of the same webhook event. Publishers MUST generate a cryptographically random value (UUID v4 recommended) per distinct event and reuse the same key on every retry of that event. Receivers MUST dedupe by this key, scoped to the authenticated sender identity (HMAC secret or Bearer credential) — keys from different publishers are independent. This is the canonical dedup field — the (task_id, status, timestamp) tuple is insufficient when a single transition is retried with unchanged timestamp or when two transitions share a timestamp.",
      "minLength": 16,
      "maxLength": 255,
      "pattern": "^[A-Za-z0-9_.:-]{16,255}$"
    },
    "notification_id": {
      "type": "string",
      "description": "Event-layer identifier for one logical notification. Stable across re-emissions of the same logical event and distinct from the per-fire `idempotency_key` issued at the transport layer. Receivers MUST track both when this field is present: `idempotency_key` suppresses transport retries, while `notification_id` recognizes a repeated logical event. Seeing the same `notification_id` under two different `idempotency_key` values is a re-emission signal, not a transport retry. Population and the separate resource key used for repair are event-shape-dependent (see notification-type.json enumDescriptions): impairment aliases `impairment_id`, creative and account notifications use transition identifiers and repair by `creative_id` or `account_id`, wholesale events alias `event.event_id` and repair by feed scope/version, and capability changes use a revision-event identifier and repair against `capabilities_version`. Point-in-time delivery events (`scheduled`, `final`, `delayed`, `adjusted`, `window_update`) omit this field and dedupe only by `idempotency_key`. Future notification types declare their identity and repair-key semantics in notification-type.json. Charset is constrained to `[A-Za-z0-9_.:-]` — the same safe-to-log/safe-to-concat character class as `idempotency_key` — so receivers can write this value into log lines, dashboard URLs, and LLM prompts without escaping.",
      "minLength": 1,
      "maxLength": 255,
      "pattern": "^[A-Za-z0-9_.:-]{1,255}$"
    },
    "operation_id": {
      "type": "string",
      "description": "Client-generated correlation identifier for the operation that produced this webhook. Buyers supply this value at webhook registration time via `push_notification_config.operation_id`; sellers MUST echo it verbatim in every webhook payload. Sellers MUST NOT derive `operation_id` by parsing `push_notification_config.url` — the URL is opaque to the seller. Receivers MAY dispatch endpoints by URL path or query string, but MUST correlate the operation using this payload field, not URL-derived values. See [Webhooks — Operation IDs and URL templates](/docs/building/by-layer/L3/webhooks#operation-ids-and-url-templates) for the full normative wire contract."
    },
    "task_id": {
      "type": "string",
      "description": "Unique identifier for this task. Use this to correlate webhook notifications with the original task submission.",
      "x-entity": "task"
    },
    "task_type": {
      "$ref": "https://adcontextprotocol.org/schemas/3.2.0-beta.0/enums/task-type.json",
      "description": "Type of AdCP operation that triggered this webhook. Enables webhook handlers to route to appropriate processing logic."
    },
    "protocol": {
      "$ref": "https://adcontextprotocol.org/schemas/3.2.0-beta.0/enums/adcp-protocol.json",
      "description": "AdCP protocol this task belongs to. Helps classify the operation type at a high level."
    },
    "status": {
      "$ref": "https://adcontextprotocol.org/schemas/3.2.0-beta.0/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."
    },
    "token": {
      "type": "string",
      "description": "Authentication token echoed verbatim from [`PushNotificationConfig.token`](/schemas/core/push-notification-config.json). Receivers that configured a token MUST compare it to this value to validate request authenticity, and SHOULD use a constant-time equality check to mitigate timing attacks. Absent when no token was configured at registration. Length bounds mirror the config-side field — receivers MAY reject payloads whose token length falls outside the configured range as a defensive check, provided the length check is performed only after the configured token is known to exist for this subscription, and the length comparison is not used as a fast-path to short-circuit the constant-time compare on equal-length inputs. Receivers MUST NOT treat absence as an authenticity failure when no token was configured.",
      "minLength": 16,
      "maxLength": 4096
    },
    "result": {
      "$ref": "https://adcontextprotocol.org/schemas/3.2.0-beta.0/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": [
    "idempotency_key",
    "operation_id",
    "task_id",
    "task_type",
    "status",
    "timestamp"
  ],
  "additionalProperties": true,
  "examples": [
    {
      "description": "Webhook for input-required status (human approval needed)",
      "data": {
        "idempotency_key": "whk_01HW9D2T3VXQ5M7K9N1P3R5S7U",
        "operation_id": "op_456",
        "task_id": "task_456",
        "task_type": "create_media_buy",
        "protocol": "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": {
        "idempotency_key": "whk_01HW9D3H8FZP2N6R8T0V4X6Z9B",
        "operation_id": "op_456",
        "task_id": "task_456",
        "task_type": "create_media_buy",
        "protocol": "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",
          "creative_deadline": "2024-01-30T23:59:59Z",
          "packages": [
            {
              "package_id": "pkg_12345_001",
              "product_id": "ctv_sports_premium",
              "budget": 60000,
              "pacing": "even",
              "pricing_option_id": "cpm-fixed-sports",
              "paused": false,
              "context": {
                "buyer_ref": "line-001"
              },
              "creative_assignments": [],
              "formats_to_provide": [
                {
                  "format_kind": "video_hosted",
                  "format_option_id": "video_standard_30s",
                  "params": {
                    "duration_ms_exact": 30000,
                    "containers": [
                      "mp4"
                    ]
                  }
                }
              ]
            }
          ]
        }
      }
    },
    {
      "description": "Webhook for working status with progress",
      "data": {
        "idempotency_key": "whk_01HW9D4K5RMS7P8T2V4X6Z8B0D",
        "operation_id": "op_456",
        "task_id": "task_456",
        "task_type": "create_media_buy",
        "protocol": "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": {
        "idempotency_key": "whk_01HW9D5N9TQV4M6P8R0T2V4X6Z",
        "operation_id": "op_789",
        "task_id": "task_789",
        "task_type": "sync_creatives",
        "protocol": "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"
            }
          ]
        }
      }
    }
  ]
}
