{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://adcontextprotocol.org/schemas/3.2.0-beta.5/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.5/media-buy/get-products-rejected.json"
          }
        }
      }
    }
  ],
  "properties": {
    "idempotency_key": {
      "type": "string",
      "description": "Sender-generated delivery key stable across RFC 8785 JCS-equivalent retries of the complete authenticated webhook payload. Publishers MUST generate a cryptographically random value (UUID v4 recommended), bind it immutably to the first canonical payload for the advertised delivery retry horizon, and use a fresh key for a changed payload or distinct delivery. Receivers scope the binding to the authenticated sender identity. Same key plus identical payload while active returns retryable 503; after durable acknowledgement it returns 2xx; same key plus a different canonical payload returns non-retryable 409. This is the transport delivery identity, not request idempotency or stable logical notification identity.",
      "minLength": 16,
      "maxLength": 255,
      "pattern": "^[A-Za-z0-9_.:-]{16,255}$"
    },
    "notification_id": {
      "type": "string",
      "description": "Optional event-layer identifier for one logical notification. Stable across re-emissions of the same logical event and distinct from the per-delivery `idempotency_key`. For terminal task webhooks, the authoritative terminal identity remains the authenticated seller plus the bound task_id; when notification_id is present, different delivery keys carrying the same value are re-emissions and MUST NOT republish terminal effects. For other event families, population and repair identity remain event-shape-dependent (see notification-type.json enumDescriptions): impairment aliases impairment_id, creative and account notifications use transition identifiers, wholesale events alias event.event_id, and capability changes use a revision-event identifier. Point-in-time delivery events (scheduled, final, delayed, adjusted, window_update) omit this field and dedupe by idempotency_key plus their delivery-report identity. Charset is constrained to `[A-Za-z0-9_.:-]`.",
      "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.5/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.5/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.5/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 logical webhook delivery was first generated. Every retry under the same idempotency_key MUST repeat this exact body value, along with every other payload member; only transport/signature metadata such as a fresh RFC 9421 nonce or created parameter may change between attempts."
    },
    "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 correlation identifier. This value alone is not continuation authority and MUST NOT be used to resume input-required or auth-required work without the verified native transport identity required by that transport."
    },
    "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.5/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",
        "notification_id": "task_456.terminal",
        "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",
        "notification_id": "task_789.terminal",
        "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"
            }
          ]
        }
      }
    }
  ]
}
