{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://adcontextprotocol.org/schemas/3.2.0-beta.0/core/protocol-envelope.json",
  "title": "Protocol Envelope",
  "description": "Canonical envelope field-set for AdCP task responses, normalized across transports. Defines the protocol-layer fields (status, context_id, context, task_id, timestamp, replayed, adcp_error, push_notification_config, governance_context) and the conceptual `payload` grouping for task-specific response data. The serialization rules — whether envelope fields appear as siblings of payload fields, as a nested `payload` object, or via transport-native containers — are transport-specific and normative per transport (see Transport serialization below). The `status` field is REQUIRED on every task response envelope, including synchronous metadata responses (e.g., `get_adcp_capabilities`) where the value is `completed`. Agents shipping responses without a top-level `status` are non-conformant regardless of whether the task body schema would otherwise validate.",
  "type": "object",
  "properties": {
    "context_id": {
      "type": "string",
      "description": "Session/conversation identifier for tracking related operations across multiple task invocations. Managed by the protocol layer to maintain conversational context. Distinct from `context` (per-request opaque echo, see below)."
    },
    "context": {
      "$ref": "https://adcontextprotocol.org/schemas/3.2.0-beta.0/core/context.json",
      "description": "Per-request opaque caller-supplied correlation object echoed unchanged in the response. Used for buyer-side tracking (UI session IDs, trace IDs, custom metadata) that the agent MUST preserve byte-for-byte without parsing. Distinct from `context_id` (server-managed session identifier) — `context` is caller-owned echo, `context_id` is server-owned session scope. Both MAY appear on the same response.\n\n**Relationship to per-task body-level `context` declarations.** Many task request/response schemas (147 as of 3.1) already declare a body-level `context` field that `$ref`s `/schemas/core/context.json` at the body root. Under the flat-on-the-wire MCP serialization (see `notes` below), envelope-level `context` and body-level `context` occupy the same key on the response root — they are NOT separate fields, they MUST share the same value, and they MUST both `$ref` `core/context.json`. The envelope declaration is **authoritative** for the schema definition; per-task body declarations are mirrors retained for tooling reasons (SDK codegen completeness, per-task validation against the response schema in isolation). Future versions MAY drop body-level `context` declarations from per-task schemas; conformance does not require either declaration to be present, only that the wire value `$ref`s `core/context.json`."
    },
    "task_id": {
      "type": "string",
      "description": "Unique identifier for tracking asynchronous operations. Present when a task requires extended processing time. Used to query task status and retrieve results when complete.",
      "x-entity": "task"
    },
    "status": {
      "$ref": "https://adcontextprotocol.org/schemas/3.2.0-beta.0/enums/task-status.json",
      "description": "Current AdCP task state or structured outcome. Indicates whether the task completed, is in progress, was submitted for async processing, failed, requires user input, or returned a typed business rejection. REQUIRED on every task response envelope. Synchronous tasks (including read-only metadata calls like `get_adcp_capabilities`) normally emit `status: \"completed\"`; a task-specific rejection arm emits `status: \"rejected\"` without turning the transport into a failure. Async tasks emit `submitted`, `working`, `input-required`, etc. per their lifecycle. Agents MUST NOT emit the legacy task_status or response_status fields alongside this field — the status field is the single authoritative AdCP response state."
    },
    "message": {
      "type": "string",
      "description": "Human-readable summary of the task result. Provides natural language explanation of what happened, suitable for display to end users or for AI agent comprehension. Generated by the protocol layer based on the task response."
    },
    "timestamp": {
      "type": "string",
      "format": "date-time",
      "description": "ISO 8601 timestamp when the response was generated. Useful for debugging, logging, cache validation, and tracking async operation progress."
    },
    "replayed": {
      "type": "boolean",
      "description": "Set to true when this response was returned from the idempotency cache rather than from a fresh execution. Set to false (or omitted) when the request was executed fresh. Buyers use this to distinguish cached replays from new executions — matters for billing reconciliation, audit logs, state-machine routing (cached state-tracking fields are historical snapshots, not current state — re-read via the resource's read endpoint), and any downstream system that assumes exactly-once event semantics. From 3.1 onward, `replayed` MAY appear on responses to any request that resolved via the idempotency cache, including read tools — universal `idempotency_key` (see security.mdx §Idempotency) means the cache holds read responses too.",
      "default": false
    },
    "adcp_error": {
      "$ref": "https://adcontextprotocol.org/schemas/3.2.0-beta.0/core/error.json",
      "description": "Transport-envelope error signal for fatal task failures. Per the two-layer model in `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`, a fatal task failure SHOULD populate both this envelope-level field AND the payload's `errors[]` array — the envelope carries a typed, extractable error so MCP/A2A clients can dispatch without re-parsing the payload, while the payload's structured `errors[]` remains the canonical normative shape. Non-fatal warnings populate ONLY `payload.errors[]` with `severity: warning` — the envelope MUST NOT carry `adcp_error` for non-failures."
    },
    "push_notification_config": {
      "$ref": "https://adcontextprotocol.org/schemas/3.2.0-beta.0/core/push-notification-config.json",
      "description": "Push notification configuration for async task updates (A2A and REST protocols). Echoed from the request to confirm webhook settings. Specifies URL, authentication scheme (Bearer or HMAC-SHA256), and credentials. MCP uses progress notifications instead of webhooks."
    },
    "governance_context": {
      "type": "string",
      "description": "Opaque authorization context issued only by an approved check_governance decision. Buyers attach it to governed requests across protocol roles (media buys, rights acquisitions, signal activations, creative services); receiving services persist it and forward it on subsequent execution and lifecycle checks. The context is the authoritative plan binding at service boundaries, so a service MUST NOT require a separate plan_id.\n\nGovernance agents MUST emit a compact JWS per the AdCP JWS profile. Verifiers validate standard authorization claims such as signature, issuer, audience, expiry, and replay protection, but intermediaries MUST NOT interpret embedded governance state for business logic. A conditions or denied verdict never carries an authorization context.\n\nThis is the primary correlation key for audit and reporting across the governance lifecycle.",
      "minLength": 1,
      "maxLength": 4096,
      "pattern": "^[\\x20-\\x7E]+$"
    },
    "payload": {
      "type": "object",
      "description": "Conceptual grouping for the task-specific response data defined by individual task response schemas (e.g., get-products-response.json, create-media-buy-response.json). `payload` is a documentary construct — it is NOT a required wire field, and its on-the-wire shape depends on transport (see Transport serialization below). Task response schemas declare body fields without wrapping them in a `payload` object; the wire representation places those body fields per transport convention. On MCP the body fields appear as siblings of envelope fields at the root of the tool response; on A2A they appear inside `task.artifacts[0].parts[].DataPart`; on REST they appear at the root of the JSON body.",
      "x-adcp-open-payload": true,
      "additionalProperties": true
    }
  },
  "required": [
    "status"
  ],
  "additionalProperties": true,
  "not": {
    "anyOf": [
      {
        "required": [
          "task_status"
        ]
      },
      {
        "required": [
          "response_status"
        ]
      }
    ]
  },
  "examples": [
    {
      "description": "Synchronous task response with immediate results",
      "data": {
        "context_id": "ctx_abc123",
        "status": "completed",
        "message": "Found 3 products matching your criteria for CTV inventory in California",
        "timestamp": "2025-10-14T14:25:30Z",
        "payload": {
          "products": [
            {
              "product_id": "ctv_premium_ca",
              "name": "CTV Premium - California",
              "description": "Premium connected TV inventory across California",
              "pricing": {
                "model": "cpm",
                "amount": 45,
                "currency": "USD"
              }
            }
          ]
        }
      }
    },
    {
      "description": "Asynchronous task response with pending operation",
      "data": {
        "context_id": "ctx_def456",
        "task_id": "task_789",
        "status": "submitted",
        "message": "Media buy creation submitted. Processing will take approximately 5-10 minutes. You'll receive updates via webhook.",
        "timestamp": "2025-10-14T14:30:00Z",
        "push_notification_config": {
          "url": "https://buyer.example.com/webhooks/adcp",
          "authentication": {
            "schemes": [
              "HMAC-SHA256"
            ],
            "credentials": "shared_secret_exchanged_during_onboarding_min_32_chars"
          }
        },
        "payload": {
          "account": {
            "account_id": "acct_123"
          }
        }
      }
    },
    {
      "description": "Task response requiring user input",
      "data": {
        "context_id": "ctx_ghi789",
        "task_id": "task_101",
        "status": "input-required",
        "message": "This media buy requires manual approval. Please review the terms and confirm to proceed.",
        "timestamp": "2025-10-14T14:32:15Z",
        "payload": {
          "media_buy_id": "mb_123456",
          "packages": [
            {
              "package_id": "pkg_001"
            }
          ],
          "errors": [
            {
              "code": "APPROVAL_REQUIRED",
              "message": "Budget exceeds auto-approval threshold",
              "severity": "warning"
            }
          ]
        }
      }
    },
    {
      "description": "Idempotent replay — same key and payload as a prior request within the replay window",
      "data": {
        "context_id": "ctx_abc123",
        "status": "completed",
        "message": "Returning cached response for idempotency_key (already processed)",
        "timestamp": "2025-10-14T14:35:00Z",
        "replayed": true,
        "payload": {
          "media_buy_id": "mb_01HW7J8K9P0Q1R2S3T4U5V6W7X"
        }
      }
    },
    {
      "description": "Failed task response with error details",
      "data": {
        "context_id": "ctx_jkl012",
        "status": "failed",
        "message": "Unable to create media buy due to invalid targeting parameters",
        "timestamp": "2025-10-14T14:28:45Z",
        "payload": {
          "errors": [
            {
              "code": "INVALID_TARGETING",
              "message": "Geographic targeting codes are invalid",
              "field": "targeting.geo_countries",
              "severity": "error"
            }
          ]
        }
      }
    }
  ],
  "notes": [
    "Task response schemas (e.g., get-products-response.json) define ONLY the body fields; protocol-layer fields live on this envelope.",
    "Transport serialization (normative):",
    "  - MCP: envelope fields and task-body fields are siblings at the root of the tool response. The `payload` object is NOT serialized as a nested key — its body fields are flattened to the root alongside `status`, `context_id`, `context`, etc. This matches MCP's native `structuredContent` convention and is what shipping SDKs (@adcp/client) emit. Conformant MCP receivers parse from the flat root; receivers that expect a nested `payload` key MUST migrate.",
    "  - A2A 1.0 with the AdCP profile extension: `task.status.state` carries the transport invocation lifecycle used to select the extraction location; `task.contextId` carries `context_id`, while `task.id` is an independent A2A transport identifier. The complete AdCP response, including its protocol-level `status` and any application-layer `task_id`, is canonically carried in `task.artifacts[0].parts[].DataPart` on final states; `task.status.message.parts[].DataPart` is the fallback container used only for interim A2A states (working, native submitted, input-required) where no final artifact has been emitted yet. For a structured AdCP business rejection or Submitted response, A2A is `completed` while the DataPart carries `status: rejected` or `status: submitted` because the handler invocation succeeded. An AdCP `task_id` MUST NOT be copied into artifact metadata or inferred from `task.id`. Receivers MUST prefer artifacts when present and MUST NOT overwrite a DataPart's status with the A2A state. See `a2a-profile-extension.mdx` and `a2a-response-extraction.mdx` for the full binding and extraction algorithm.",
    "  - REST: envelope fields MAY ride on HTTP headers (e.g., `X-AdCP-Status`, `X-AdCP-Context-Id`) or as JSON body siblings; body fields appear at the JSON body root. Implementers choosing the header path SHOULD also mirror to body siblings for non-streaming callers.",
    "Across all three: envelope and body fields are conceptually a single response object. A task response schema MAY declare body fields with the same name as envelope fields (e.g., `errors[]` body-level for per-record validation results vs envelope-level for fatal task failure) and the two MUST be treated as distinct fields by name within their respective namespaces — see `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`.",
    "`status` is REQUIRED on the conceptual AdCP response across all transports. On MCP and REST it appears as a sibling field at the JSON root (or `structuredContent` root for MCP). On A2A it is carried inside the authoritative DataPart alongside the task body; `task.status.state` separately carries the A2A transport lifecycle and selects where that DataPart is found. Receivers MUST preserve the DataPart status when present. The schema-level `required: [status]` enforces the post-extraction AdCP shape. `payload` remains intentionally NOT required — it is a documentary grouping construct, never a required wire field. See `mcp-guide.mdx` and `a2a-guide.mdx` for the wire-level patterns receivers MUST implement.",
    "Receivers MUST handle absence of an envelope field (e.g., `replayed` omitted) as the field's documented default — see each field's `default` clause."
  ]
}
