{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "/schemas/3.1.0-rc.4/core/mcp-webhook-payload.json",
  "title": "MCP Webhook Payload",
  "description": "Standard envelope for HTTP-based push notifications (MCP). This defines the wire format sent to the URL configured in `pushNotificationConfig`. NOTE: This envelope is NOT used in A2A integration, which uses native Task/TaskStatusUpdateEvent messages with the AdCP payload nested in `status.message.parts[].data`.",
  "type": "object",
  "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, per-state-event identifier. Stable across re-emissions of the same logical event — distinct from the per-fire `idempotency_key` issued at the transport layer. Receivers MUST track both: `idempotency_key` suppresses transport retries; `notification_id` correlates fires to current snapshot state. Seeing the same `notification_id` under two different `idempotency_key` values is a re-emission signal (e.g., the seller is re-firing because a prior fire was unreachable), not a transport retry — receivers SHOULD treat that as a missed-events warning rather than collapsing it. Population is event-shape-dependent (see notification-type.json enumDescriptions for per-type values): for state-shaped events (e.g., `impairment`), this equals the resource's stable id (e.g., `impairment_id`); for point-in-time data events with no persistent state id (e.g., `scheduled`/`final`/`delayed`/`adjusted` delivery report fires per snapshot-and-log Rule 1), this field is absent — the per-fire `idempotency_key` is all there is. Future notification types declare their per-type population in notification-type.json enumDescriptions. 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 MUST correlate webhooks using this payload field, never URL-path inspection. 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": "/schemas/3.1.0-rc.4/enums/task-type.json",
      "description": "Type of AdCP operation that triggered this webhook. Enables webhook handlers to route to appropriate processing logic."
    },
    "protocol": {
      "$ref": "/schemas/3.1.0-rc.4/enums/adcp-protocol.json",
      "description": "AdCP protocol this task belongs to. Helps classify the operation type at a high level."
    },
    "status": {
      "$ref": "/schemas/3.1.0-rc.4/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": "/schemas/3.1.0-rc.4/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": [],
              "format_ids_to_provide": [
                {
                  "agent_url": "https://creative.adcontextprotocol.org",
                  "id": "video_standard_30s"
                }
              ]
            }
          ]
        }
      }
    },
    {
      "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"
            }
          ]
        }
      }
    }
  ]
}
