Package adcp

Sub-modules

adcp.adagents
adcp.audit_sink

Audit-event observability seam paralleling :class:~adcp.webhook_supervisor.DeliveryLogSink

adcp.canonical_formats

Pythonic v1↔v2 canonical-formats projection layer …

adcp.capabilities

Feature capability resolution for AdCP …

adcp.client
adcp.compat

AdCP compatibility surfaces for buyers on older spec versions …

adcp.config
adcp.decisioning

Decisioning Platform v6.0 — Protocol-driven adopter framework …

adcp.error_sanitization

Sanitizers for public error/account authorization metadata.

adcp.exceptions

Exception hierarchy for AdCP client.

adcp.feed_mirror

In-memory mirror of an AdCP agent's wholesale product and signal feeds …

adcp.migrate

Migration tooling for the AdCP SDK …

adcp.property_registry

PropertyRegistry — local authorization cache backed by the AAO registry …

adcp.protocols
adcp.registry

Client for the AdCP registry API (brand, property, member, and policy lookups).

adcp.registry_sync

Registry change feed synchronization.

adcp.schemas

Access bundled AdCP JSON schemas by name …

adcp.server

ADCP Server Framework …

adcp.signing

AdCP RFC 9421 request-signing profile …

adcp.simple

Simplified API accessor for ADCPClient …

adcp.substitution

Producer-side translation of AdCP universal macros in pixel URLs …

adcp.testing

Test helpers for AdCP client library …

adcp.types

AdCP Type System …

adcp.utils
adcp.validation

AdCP validation helpers …

adcp.webhook_auth

Auth-mode strategies for :class:WebhookSender

adcp.webhook_receiver

One-call webhook receiver: verify signature, dedupe, parse …

adcp.webhook_sender

One-call outbound webhook delivery for AdCP senders …

adcp.webhook_supervisor

Webhook delivery supervisor — retry, circuit breaker, attempt audit …

adcp.webhook_supervisor_pg

PostgreSQL-backed :class:WebhookDeliverySupervisor for a durable retry queue …

adcp.webhook_transport_hooks

Pre-SSRF URL rewrite hooks for :class:WebhookSender

adcp.webhooks

Webhook creation, signing, and reception for AdCP agents …

Functions

async def challenge_webhook_destination(*,
url: str | AnyUrl,
account_id: str,
subscriber_id: str,
sender: WebhookSender | None = None,
authentication: AdCPBaseModel | Mapping[str, Any] | None = None,
challenge: str | None = None,
timeout_seconds: float | None = None,
policy: WebhookDestinationPolicy | None = None,
field: str | None = None,
extra_headers: Mapping[str, str] | None = None) ‑> WebhookChallengeResult
Expand source code
async def challenge_webhook_destination(
    *,
    url: str | AnyUrl,
    account_id: str,
    subscriber_id: str,
    sender: WebhookSender | None = None,
    authentication: AdCPBaseModel | Mapping[str, Any] | None = None,
    challenge: str | None = None,
    timeout_seconds: float | None = None,
    policy: WebhookDestinationPolicy | None = None,
    field: str | None = None,
    extra_headers: Mapping[str, str] | None = None,
) -> WebhookChallengeResult:
    """Validate and prove control of a durable webhook destination.

    Use before activating a new or changed active
    ``sync_accounts.accounts[].notification_configs[]`` entry. Inactive
    configs can be persisted without calling this helper.

    ``authentication`` follows the durable config's legacy auth selector:
    when present, the challenge is sent with Bearer or HMAC-SHA256. When
    omitted, pass an RFC 9421 :class:`WebhookSender`; the helper uses that
    sender's webhook-signing key and the SDK-managed pinned transport.
    """

    error_url = str(url) if isinstance(url, (str, AnyUrl)) else None
    if sender is not None and authentication is not None:
        raise WebhookChallengeError(
            "pass either sender= for RFC 9421 or authentication= for legacy auth, not both",
            reason="ambiguous_auth_mode",
            field=field,
            url=error_url,
        )
    sender_owns_client = bool(getattr(cast(Any, sender), "_owns_client", False))
    sender_transport_hooks = tuple(getattr(cast(Any, sender), "_transport_hooks", ()))
    if sender is not None and not sender_owns_client:
        raise WebhookChallengeError(
            "proof-of-control requires a WebhookSender constructed without client=",
            reason="unsafe_sender_client",
            field=field,
            url=error_url,
        )
    if sender is not None and sender_transport_hooks:
        raise WebhookChallengeError(
            "proof-of-control does not support sender transport_hooks",
            reason="unsupported_sender_hooks",
            field=field,
            url=error_url,
        )
    if sender is not None and not sender.signs_with_rfc9421:
        raise WebhookChallengeError(
            "proof-of-control requires an RFC 9421 WebhookSender when authentication is omitted",
            reason="sender_auth_mode_mismatch",
            field=field,
            url=error_url,
            suggestion=(
                "Use WebhookSender.from_jwk(...) for default durable configs, "
                "or pass config.authentication for legacy Bearer/HMAC configs."
            ),
        )
    if sender is None and authentication is None:
        raise WebhookChallengeError(
            "webhook challenge requires sender= when authentication is omitted",
            reason="sender_required",
            field=field,
            url=error_url,
            suggestion=(
                "Pass the seller's WebhookSender, or pass config.authentication " "for legacy auth."
            ),
        )
    try:
        destination = validate_webhook_destination_url(url, policy=policy, field=field)
        payload = create_webhook_challenge_payload(
            account_id=account_id,
            subscriber_id=subscriber_id,
            challenge=challenge,
        )
    except WebhookDestinationValidationError as exc:
        raise WebhookChallengeError(
            str(exc),
            reason=exc.reason,
            field=exc.field,
            url=exc.url,
            suggestion=exc.suggestion,
        ) from exc
    except ValueError as exc:
        raise WebhookChallengeError(
            f"webhook challenge configuration is invalid: {exc}",
            reason="invalid_configuration",
            field=field,
            url=error_url,
        ) from exc
    challenge_value = payload["challenge"]

    try:
        if sender is not None:
            effective_timeout = (
                timeout_seconds
                if timeout_seconds is not None
                else float(getattr(cast(Any, sender), "_timeout", _DEFAULT_TIMEOUT_SECONDS))
            )
            response = await _send_sender_webhook_challenge(
                url=destination.effective_url,
                sender=sender,
                payload=payload,
                timeout_seconds=effective_timeout,
                policy=destination.policy,
                extra_headers=extra_headers,
            )
            status_code = response.status_code
            response_headers = dict(response.headers)
            response_body = response.content
        else:
            auth_config = _authentication_to_config(cast(Any, authentication))
            response = await _send_legacy_webhook_challenge(
                url=destination.effective_url,
                authentication=auth_config,
                payload=payload,
                extra_headers=extra_headers,
                timeout_seconds=timeout_seconds,
                policy=destination.policy,
            )
            status_code = response.status_code
            response_headers = dict(response.headers)
            response_body = response.content
    except httpx.TimeoutException as exc:
        raise WebhookChallengeError(
            "webhook challenge timed out",
            reason="timeout",
            field=field,
            url=destination.original_url,
        ) from exc
    except httpx.HTTPError as exc:
        raise WebhookChallengeError(
            f"webhook challenge request failed: {exc}",
            reason="request_failed",
            field=field,
            url=destination.original_url,
        ) from exc
    except ValueError as exc:
        raise WebhookChallengeError(
            f"webhook challenge configuration is invalid: {exc}",
            reason="invalid_configuration",
            field=field,
            url=destination.original_url,
        ) from exc

    if not 200 <= status_code < 300:
        raise WebhookChallengeError(
            f"webhook challenge failed with HTTP {status_code}",
            reason="http_status",
            field=field,
            url=destination.original_url,
            status_code=status_code,
        )

    echoed_field = validate_webhook_challenge_response(
        response_body,
        challenge=challenge_value,
        field=field,
        url=destination.original_url,
    )
    return WebhookChallengeResult(
        challenge=challenge_value,
        echoed_field=echoed_field,
        destination=destination,
        status_code=status_code,
        response_headers=response_headers,
        response_body=response_body,
    )

Validate and prove control of a durable webhook destination.

Use before activating a new or changed active sync_accounts.accounts[].notification_configs[] entry. Inactive configs can be persisted without calling this helper.

authentication follows the durable config's legacy auth selector: when present, the challenge is sent with Bearer or HMAC-SHA256. When omitted, pass an RFC 9421 :class:WebhookSender; the helper uses that sender's webhook-signing key and the SDK-managed pinned transport.

def create_a2a_webhook_payload(task_id: str,
status: TaskStatus,
context_id: str,
result: PydanticBaseModel | dict[str, Any],
timestamp: datetime | None = None) ‑> a2a_pb2.Task | a2a_pb2.TaskStatusUpdateEvent
Expand source code
def create_a2a_webhook_payload(
    task_id: str,
    status: GeneratedTaskStatus,
    context_id: str,
    result: PydanticBaseModel | dict[str, Any],
    timestamp: datetime | None = None,
) -> Task | TaskStatusUpdateEvent:
    """
    Create A2A webhook payload (Task or TaskStatusUpdateEvent).

    Per A2A specification:
    - Terminated statuses (completed, failed, canceled, rejected): Returns Task
      with artifacts[].parts[]
    - Intermediate statuses (working, input-required, submitted, auth-required):
      Returns TaskStatusUpdateEvent with status.message.parts[]

    This function helps agent implementations construct properly formatted A2A webhook
    payloads for sending to clients.

    Args:
        task_id: Unique identifier for the task
        status: Current task status
        context_id: Session/conversation identifier (required by A2A protocol)
        timestamp: When the webhook was generated (defaults to current UTC time)
        result: Task-specific payload — any Pydantic model or plain dict

    Returns:
        Task object for terminated statuses, TaskStatusUpdateEvent for intermediate statuses

    Examples:
        Create a completed Task webhook:
        >>> from adcp.webhooks import create_a2a_webhook_payload
        >>> from adcp.types import GeneratedTaskStatus
        >>>
        >>> task = create_a2a_webhook_payload(
        ...     task_id="task_123",
        ...     context_id="ctx_123",
        ...     status=GeneratedTaskStatus.completed,
        ...     result={"products": [...]},
        ... )
        >>> # task is a Task object with artifacts containing the result

        Create a working status update:
        >>> event = create_a2a_webhook_payload(
        ...     task_id="task_456",
        ...     context_id="ctx_456",
        ...     status=GeneratedTaskStatus.working,
        ...     result={"current_step": "processing", "percentage": 30},
        ... )
        >>> # event is a TaskStatusUpdateEvent with status.message

        Send A2A webhook via HTTP POST:
        >>> import httpx
        >>> from a2a.types import Task
        >>>
        >>> payload = create_a2a_webhook_payload(...)
        >>> # Serialize to dict for JSON
        >>> if isinstance(payload, Task):
        ...     payload_dict = payload.model_dump(mode='json')
        ... else:
        ...     payload_dict = payload.model_dump(mode='json')
        >>>
        >>> response = await httpx.post(webhook_url, json=payload_dict)
    """
    if timestamp is None:
        timestamp = datetime.now(timezone.utc)

    # Convert datetime to ISO string for A2A protocol
    timestamp_str = timestamp.isoformat() if isinstance(timestamp, datetime) else timestamp
    timestamp_proto = _isoformat_to_proto_timestamp(timestamp_str) if timestamp_str else None

    # Map GeneratedTaskStatus to A2A TaskState enum value.
    # GeneratedTaskStatus is always an Enum so .value is guaranteed.
    status_value = status.value
    adcp_to_task_state: dict[str, int] = {
        "completed": pb.TaskState.TASK_STATE_COMPLETED,
        "failed": pb.TaskState.TASK_STATE_FAILED,
        "canceled": pb.TaskState.TASK_STATE_CANCELED,
        "rejected": pb.TaskState.TASK_STATE_REJECTED,
        "working": pb.TaskState.TASK_STATE_WORKING,
        "submitted": pb.TaskState.TASK_STATE_SUBMITTED,
        # GeneratedTaskStatus enum values are hyphenated ("input-required",
        # "auth-required"). The underscore forms are accepted as a convenience
        # for callers passing raw strings rather than enum members.
        "input_required": pb.TaskState.TASK_STATE_INPUT_REQUIRED,
        "input-required": pb.TaskState.TASK_STATE_INPUT_REQUIRED,
        "auth_required": pb.TaskState.TASK_STATE_AUTH_REQUIRED,
        "auth-required": pb.TaskState.TASK_STATE_AUTH_REQUIRED,
    }
    task_state_enum = adcp_to_task_state.get(status_value)
    if task_state_enum is None:
        # Falling back to TASK_STATE_UNSPECIFIED (proto3 zero) would be
        # silently omitted by MessageToDict, producing an invalid wire
        # shape ``{"status": {}}`` that A2A v0.3 receivers reject as
        # missing the required ``state`` field. Fail loud at the builder
        # boundary so callers can't ship a broken envelope.
        known = [
            "submitted",
            "working",
            "input-required",
            "completed",
            "canceled",
            "failed",
            "rejected",
            "auth-required",
        ]
        raise ValueError(
            f"create_a2a_webhook_payload: unknown status {status_value!r}. "
            f"Known AdCP→A2A states: {known}. "
            "Note: 'unknown' has no a2a-sdk 1.0 protobuf constant; build a "
            "Task manually and pass it through to_wire_dict if you need to "
            "emit that state."
        )

    # Build parts for the message/artifact.
    parts: list[pb.Part] = []

    # Convert Pydantic model to dict if needed
    if hasattr(result, "model_dump"):
        result_dict: dict[str, Any] = result.model_dump(mode="json")
    else:
        result_dict = result

    value = Value()
    ParseDict(result_dict, value)
    parts.append(pb.Part(data=value))

    # Determine if this is a terminated status (Task) or intermediate (TaskStatusUpdateEvent).
    # canceled and rejected are terminal: the task will not continue.
    is_terminated = status in (
        GeneratedTaskStatus.completed,
        GeneratedTaskStatus.failed,
        GeneratedTaskStatus.canceled,
        GeneratedTaskStatus.rejected,
    )

    if is_terminated:
        status_kwargs: dict[str, Any] = {"state": task_state_enum}
        if timestamp_proto is not None:
            status_kwargs["timestamp"] = timestamp_proto
        task_status = pb.TaskStatus(**status_kwargs)

        artifacts = (
            [
                pb.Artifact(
                    artifact_id=f"{task_id}_result",
                    parts=parts,
                )
            ]
            if parts
            else []
        )

        return pb.Task(
            id=task_id,
            status=task_status,
            artifacts=artifacts,
            context_id=context_id,
        )

    # Intermediate status: build a Message carrying the parts and nest it
    # inside TaskStatus.message so the event mirrors the spec shape.
    message_obj = None
    if parts:
        message_obj = pb.Message(
            message_id=f"{task_id}_msg",
            role=pb.Role.ROLE_AGENT,
            parts=parts,
        )

    status_kwargs = {"state": task_state_enum}
    if timestamp_proto is not None:
        status_kwargs["timestamp"] = timestamp_proto
    if message_obj is not None:
        status_kwargs["message"] = message_obj
    task_status = pb.TaskStatus(**status_kwargs)

    return pb.TaskStatusUpdateEvent(
        task_id=task_id,
        status=task_status,
        context_id=context_id,
    )

Create A2A webhook payload (Task or TaskStatusUpdateEvent).

Per A2A specification: - Terminated statuses (completed, failed, canceled, rejected): Returns Task with artifacts[].parts[] - Intermediate statuses (working, input-required, submitted, auth-required): Returns TaskStatusUpdateEvent with status.message.parts[]

This function helps agent implementations construct properly formatted A2A webhook payloads for sending to clients.

Args
-----=
task_id
Unique identifier for the task
status
Current task status
context_id
Session/conversation identifier (required by A2A protocol)
timestamp
When the webhook was generated (defaults to current UTC time)
result
Task-specific payload — any Pydantic model or plain dict

Returns -----= Task object for terminated statuses, TaskStatusUpdateEvent for intermediate statuses

Examples -----= Create a completed Task webhook:

>>> from adcp.webhooks import create_a2a_webhook_payload
>>> from adcp.types import GeneratedTaskStatus
>>>
>>> task = create_a2a_webhook_payload(
...     task_id="task_123",
...     context_id="ctx_123",
...     status=GeneratedTaskStatus.completed,
...     result={"products": [...]},
... )
>>> # task is a Task object with artifacts containing the result

Create a working status update:

>>> event = create_a2a_webhook_payload(
...     task_id="task_456",
...     context_id="ctx_456",
...     status=GeneratedTaskStatus.working,
...     result={"current_step": "processing", "percentage": 30},
... )
>>> # event is a TaskStatusUpdateEvent with status.message

Send A2A webhook via HTTP POST:

>>> import httpx
>>> from a2a.types import Task
>>>
>>> payload = create_a2a_webhook_payload(...)
>>> # Serialize to dict for JSON
>>> if isinstance(payload, Task):
...     payload_dict = payload.model_dump(mode='json')
... else:
...     payload_dict = payload.model_dump(mode='json')
>>>
>>> response = await httpx.post(webhook_url, json=payload_dict)
def create_mcp_webhook_payload(task_id: str,
status: TaskStatus | str,
task_type: TaskType | str,
*,
result: PydanticBaseModel | dict[str, Any] | None = None,
timestamp: datetime | None = None,
operation_id: str,
notification_id: str | None = None,
message: str | None = None,
context_id: str | None = None,
protocol: AdcpProtocol | str | None = None,
idempotency_key: str | None = None,
token: str | None = None) ‑> adcp.types.generated_poc.core.mcp_webhook_payload.McpWebhookPayload
Expand source code
def create_mcp_webhook_payload(
    task_id: str,
    status: GeneratedTaskStatus | str,
    task_type: TaskType | str,
    *,
    result: PydanticBaseModel | dict[str, Any] | None = None,
    timestamp: datetime | None = None,
    operation_id: str,
    notification_id: str | None = None,
    message: str | None = None,
    context_id: str | None = None,
    protocol: AdcpProtocol | str | None = None,
    idempotency_key: str | None = None,
    token: str | None = None,
) -> McpWebhookPayload:
    """
    Build an :class:`McpWebhookPayload` for a tracked async task.

    Pair with :func:`to_wire_dict` for HTTP transport — Pydantic-typed at
    construction so the publisher catches schema drift before it leaves
    the process.

    ``task_type`` is restricted to the closed :class:`TaskType` enum (the
    spec's complete set of async/tracked operations). Passing a value not
    present in the enum produces a validation error before an invalid webhook
    payload can leave the process.

    Args:
        task_id: Unique identifier for the task.
        status: Current task status.
        task_type: Type of AdCP async operation (see :class:`TaskType`).
        result: Task-specific payload — any Pydantic model or plain dict.
            Plain dicts are validated against
            :class:`AdcpAsyncResponseData`'s discriminated union.
        timestamp: When the webhook was generated. Defaults to current UTC.
        operation_id: Client-generated identifier supplied through
            ``push_notification_config.operation_id``. Required for every
            task webhook; publishers echo it verbatim and MUST NOT derive it
            from the receiver URL.
        notification_id: Stable identity for one logical notification.
            Terminal task webhooks default to ``"{task_id}.terminal"`` so
            re-emissions under different delivery keys still converge.
        message: Human-readable summary of task state.
        context_id: Session/conversation identifier.
        protocol: AdCP protocol this task belongs to (see :class:`AdcpProtocol`).
            Auto-derived from ``task_type`` when omitted, matching the JS
            SDK's ``protocolForTool`` so cross-SDK bodies classify
            operations identically. Pass an explicit value to override.
        idempotency_key: Sender-generated key stable across retries of the
            same event. Defaults to a freshly-generated UUID v4 — callers
            retrying delivery of the same event MUST pass the key from
            their first attempt; passing None twice mints two keys and
            defeats dedup.
        token: Buyer-supplied token from ``push_notification_config.token``,
            echoed back per spec for authenticity validation.

    Returns:
        :class:`McpWebhookPayload` instance. Use :func:`to_wire_dict` (or
        ``payload.model_dump(mode="json", exclude_none=True)``) to get the
        JSON-ready dict for HTTP transport.

    Examples:
        Create a completed webhook with results:
        >>> from adcp.webhooks import create_mcp_webhook_payload, to_wire_dict
        >>> from adcp.types import GeneratedTaskStatus
        >>>
        >>> payload = create_mcp_webhook_payload(
        ...     task_id="task_123",
        ...     operation_id="op_123",
        ...     status=GeneratedTaskStatus.completed,
        ...     task_type="create_media_buy",
        ...     result={"media_buy_id": "mb_1", "buyer_ref": "ref_1"},
        ...     message="Created campaign"
        ... )
        >>> wire = to_wire_dict(payload)

        Create a failed webhook with error:
        >>> payload = create_mcp_webhook_payload(
        ...     task_id="task_456",
        ...     operation_id="op_456",
        ...     status=GeneratedTaskStatus.failed,
        ...     task_type="create_media_buy",
        ...     result={"errors": [{"code": "INVALID_INPUT", "message": "..."}]},
        ...     message="Validation failed"
        ... )

        Create a working status update:
        >>> payload = create_mcp_webhook_payload(
        ...     task_id="task_789",
        ...     operation_id="op_789",
        ...     status=GeneratedTaskStatus.working,
        ...     task_type="sync_creatives",
        ...     message="Processing 3 of 10 creatives"
        ... )
    """
    if timestamp is None:
        timestamp = datetime.now(timezone.utc)
    if idempotency_key is None:
        idempotency_key = generate_webhook_idempotency_key()
    if not operation_id:
        raise ValueError(
            "operation_id is required for AdCP task webhooks; copy "
            "push_notification_config.operation_id verbatim"
        )

    status_value = status.value if hasattr(status, "value") else str(status)
    if notification_id is None and status_value in {
        "completed",
        "failed",
        "canceled",
        "rejected",
    }:
        notification_id = f"{task_id}.terminal"

    # Auto-derive `protocol` from `task_type` when caller doesn't override.
    # Matches `protocolForTool` in the JS reference SDK so cross-SDK bodies
    # classify operations identically.
    if protocol is None:
        try:
            task_type_enum = task_type if isinstance(task_type, TaskType) else TaskType(task_type)
        except ValueError:
            # Unknown string — let `model_validate` raise the canonical
            # task_type error below rather than swallow it here.
            task_type_enum = None
        if task_type_enum is not None:
            protocol = _TASK_TYPE_TO_PROTOCOL.get(task_type_enum)

    # Foreign BaseModel subclasses (anything outside AdcpAsyncResponseData)
    # don't match the discriminated-union variants by identity — dump to a
    # dict so the union picks by shape, matching the dict path.
    result_value: PydanticBaseModel | dict[str, Any] | None
    if isinstance(result, PydanticBaseModel):
        result_value = result.model_dump(mode="json")
    else:
        result_value = result

    payload = McpWebhookPayload.model_validate(
        {
            "idempotency_key": idempotency_key,
            "notification_id": notification_id,
            "task_id": task_id,
            "task_type": task_type,
            "protocol": protocol,
            "status": status_value,
            "timestamp": timestamp,
            "operation_id": operation_id,
            "message": message,
            "context_id": context_id,
            "token": token,
        }
    )
    # Preserve task result payloads byte-for-byte. Validating through the
    # generated AdcpAsyncResponseData union can coerce arbitrary dicts into
    # typed response models and inject response defaults, changing webhook
    # bodies before signing.
    payload.result = result_value  # type: ignore[assignment]
    return payload

Build an :class:McpWebhookPayload for a tracked async task.

Pair with :func:to_wire_dict() for HTTP transport — Pydantic-typed at construction so the publisher catches schema drift before it leaves the process.

task_type is restricted to the closed :class:TaskType enum (the spec's complete set of async/tracked operations). Passing a value not present in the enum produces a validation error before an invalid webhook payload can leave the process.

Args
-----=
task_id
Unique identifier for the task.
status
Current task status.
task_type
Type of AdCP async operation (see :class:TaskType).
result
Task-specific payload — any Pydantic model or plain dict. Plain dicts are validated against :class:AdcpAsyncResponseData's discriminated union.
timestamp
When the webhook was generated. Defaults to current UTC.
operation_id
Client-generated identifier supplied through push_notification_config.operation_id. Required for every task webhook; publishers echo it verbatim and MUST NOT derive it from the receiver URL.
notification_id
Stable identity for one logical notification. Terminal task webhooks default to "{task_id}.terminal" so re-emissions under different delivery keys still converge.
message
Human-readable summary of task state.
context_id
Session/conversation identifier.
protocol
AdCP protocol this task belongs to (see :class:AdcpProtocol). Auto-derived from task_type when omitted, matching the JS SDK's protocolForTool so cross-SDK bodies classify operations identically. Pass an explicit value to override.
idempotency_key
Sender-generated key stable across retries of the same event. Defaults to a freshly-generated UUID v4 — callers retrying delivery of the same event MUST pass the key from their first attempt; passing None twice mints two keys and defeats dedup.
token
Buyer-supplied token from push_notification_config.token, echoed back per spec for authenticity validation.

Returns -----= :class:McpWebhookPayload instance. Use :func:to_wire_dict() (or payload.model_dump(mode="json", exclude_none=True)) to get the JSON-ready dict for HTTP transport.

Examples -----= Create a completed webhook with results:

>>> from adcp.webhooks import create_mcp_webhook_payload, to_wire_dict
>>> from adcp.types import GeneratedTaskStatus
>>>
>>> payload = create_mcp_webhook_payload(
...     task_id="task_123",
...     operation_id="op_123",
...     status=GeneratedTaskStatus.completed,
...     task_type="create_media_buy",
...     result={"media_buy_id": "mb_1", "buyer_ref": "ref_1"},
...     message="Created campaign"
... )
>>> wire = to_wire_dict(payload)

Create a failed webhook with error:

>>> payload = create_mcp_webhook_payload(
...     task_id="task_456",
...     operation_id="op_456",
...     status=GeneratedTaskStatus.failed,
...     task_type="create_media_buy",
...     result={"errors": [{"code": "INVALID_INPUT", "message": "..."}]},
...     message="Validation failed"
... )

Create a working status update:

>>> payload = create_mcp_webhook_payload(
...     task_id="task_789",
...     operation_id="op_789",
...     status=GeneratedTaskStatus.working,
...     task_type="sync_creatives",
...     message="Processing 3 of 10 creatives"
... )
def create_test_agent(**overrides: Any) ‑> AgentConfig
Expand source code
def create_test_agent(**overrides: Any) -> AgentConfig:
    """Create a custom test agent configuration.

    Useful when you need to modify the default test agent setup.

    Args:
        **overrides: Keyword arguments to override default config values

    Returns:
        Complete agent configuration

    Example:
        ```python
        from adcp.testing import create_test_agent
        from adcp.client import ADCPClient

        # Use default test agent with custom ID
        config = create_test_agent(id="my-test-agent")
        client = ADCPClient(config)
        ```

    Example:
        ```python
        # Use A2A protocol instead of MCP
        from adcp.types.core import Protocol

        config = create_test_agent(
            protocol=Protocol.A2A,
            agent_uri="https://test-agent.adcontextprotocol.org"
        )
        ```
    """
    base_config = TEST_AGENT_MCP_CONFIG.model_dump()
    base_config.update(overrides)
    return AgentConfig(**base_config)

Create a custom test agent configuration.

Useful when you need to modify the default test agent setup.

Args
-----=
**overrides
Keyword arguments to override default config values

Returns -----= Complete agent configuration

Example -----=

from adcp.testing import create_test_agent
from adcp.client import ADCPClient

# Use default test agent with custom ID
config = create_test_agent(id="my-test-agent")
client = ADCPClient(config)

Example -----=

# Use A2A protocol instead of MCP
from adcp.types.core import Protocol

config = create_test_agent(
    protocol=Protocol.A2A,
    agent_uri="https://test-agent.adcontextprotocol.org"
)
def create_webhook_challenge_payload(*, account_id: str, subscriber_id: str, challenge: str | None = None) ‑> dict[str, str]
Expand source code
def create_webhook_challenge_payload(
    *,
    account_id: str,
    subscriber_id: str,
    challenge: str | None = None,
) -> dict[str, str]:
    """Build the durable ``notification_configs[]`` challenge payload."""

    if not isinstance(account_id, str) or not account_id:
        raise ValueError("account_id must be a non-empty string")
    if not isinstance(subscriber_id, str) or not subscriber_id:
        raise ValueError("subscriber_id must be a non-empty string")
    challenge_value = generate_webhook_challenge_value() if challenge is None else challenge
    if not isinstance(challenge_value, str) or not challenge_value:
        raise ValueError("challenge must be a non-empty string")
    return {
        "type": "webhook.challenge",
        "challenge": challenge_value,
        "account_id": account_id,
        "subscriber_id": subscriber_id,
    }

Build the durable notification_configs[] challenge payload.

async def detect_publisher_properties_divergence(agent_url: str,
*,
directory_url: str,
sample_size: int | None = 200,
max_concurrency: int = 20,
timeout: float = 30.0,
client: httpx.AsyncClient | None = None) ‑> list[PublisherDivergence]
Expand source code
async def detect_publisher_properties_divergence(
    agent_url: str,
    *,
    directory_url: str,
    sample_size: int | None = 200,
    max_concurrency: int = 20,
    timeout: float = 30.0,
    client: httpx.AsyncClient | None = None,
) -> DivergenceReport:
    """Compare directory's inline resolution against per-publisher federated fetches.

    For each publisher the directory lists under ``agent_url``, fetches
    that publisher's own ``adagents.json`` and compares the property set
    against the directory's claim. Returns only publishers where the two
    paths disagree (or where the child fetch failed).

    Always requests ``include=["properties"]`` from the directory so the
    full ``(publisher_domain, property_id)`` set-diff lights up on
    directories that support adcp#4894. Against older directories that
    return only ``properties_authorized`` counts, falls back to count-
    comparison; ``missing_in_inline`` / ``missing_in_federated`` are
    None in that fallback path.

    Per adcp#4827 §Resolution-paths, the federated result is
    authoritative when the two paths disagree.

    Args:
        agent_url: agent to check.
        directory_url: AAO directory base URL (HTTPS only — same SSRF
            gate as :func:`fetch_agent_authorizations_from_directory`).
        sample_size: cap the sweep at N publishers (drawn from the first
            page of directory results). None opts into a full sweep
            across all pages — only do this for small networks. Default
            200 keeps the divergence sweep bounded by default.
        max_concurrency: semaphore-capped concurrent federated fetches.
            Default 20 — caps the burst against publisher origins.
        timeout: per-request timeout (directory + child fetches).
        client: optional shared ``httpx.AsyncClient``.

    Returns:
        :data:`DivergenceReport` (``list[PublisherDivergence]``). Empty
        list = no divergence detected. Note in count-only fallback mode,
        an empty list means counts agree but set-equality is not
        guaranteed.
    """
    http = client
    collected: list[DirectoryPublisherEntry] = []
    cursor: str | None = None
    seen_cursors: set[str] = set()
    page_count = 0
    while True:
        page = await fetch_agent_authorizations_from_directory(
            agent_url,
            directory_url=directory_url,
            cursor=cursor,
            include=["properties"],
            timeout=timeout,
            client=http,
        )
        page_count += 1
        collected.extend(page.publishers)
        if sample_size is not None and len(collected) >= sample_size:
            collected = collected[:sample_size]
            break
        cursor = page.next_cursor
        if not cursor:
            break
        if cursor in seen_cursors:
            raise AdagentsValidationError(
                f"Directory page cursor {cursor!r} repeated — refusing to loop forever."
            )
        seen_cursors.add(cursor)
        if page_count >= MAX_DIRECTORY_PAGES:
            raise AdagentsValidationError(
                f"Directory pagination exceeded {MAX_DIRECTORY_PAGES} pages — aborting sweep."
            )

    # Dedupe by publisher_domain before fan-out: a hostile directory
    # returning N rows for the same publisher would otherwise amplify
    # into N concurrent fetches against a single victim host. First
    # occurrence wins (deterministic) — conflicting property_ids /
    # properties_authorized across duplicates are dropped here; the
    # directory's behavior is itself a divergence signal for ops.
    seen_domains: set[str] = set()
    deduped: list[DirectoryPublisherEntry] = []
    for entry in collected:
        if entry.publisher_domain in seen_domains:
            continue
        seen_domains.add(entry.publisher_domain)
        deduped.append(entry)
    collected = deduped

    # Emit a one-shot warning when the entire sample comes back without
    # property_ids[]. In count-only mode, same-count substitutions are
    # undetectable — adopters should pin include=["properties"] support
    # on directories that offer it.
    if collected and all(e.property_ids is None for e in collected):
        logger.warning(
            "AAO directory %s did not return property_ids[] on any publisher "
            "entry — falling back to count-only divergence detection. Same-count "
            "substitutions are undetectable in this mode. Upgrade the directory "
            "or pin include=['properties'] support.",
            directory_url,
        )

    sem = asyncio.Semaphore(max_concurrency)

    async def _probe(entry: DirectoryPublisherEntry) -> PublisherDivergence | None:
        async with sem:
            try:
                data = await fetch_adagents(entry.publisher_domain, timeout=timeout, client=http)
                federated_props = get_properties_by_agent(data, agent_url)
                # Falsy/empty property_id is silently dropped: upstream
                # schema requires a non-empty string, so an empty value
                # is a structural violation that belongs in
                # validate_adagents, not a divergence signal. Federated
                # properties with valid IDs only.
                federated_ids = {
                    str(p.get("property_id")) for p in federated_props if p.get("property_id")
                }
            except (
                AdagentsNotFoundError,
                AdagentsValidationError,
                AdagentsTimeoutError,
                httpx.HTTPError,
                OSError,
                ValueError,
            ) as exc:
                return PublisherDivergence(
                    publisher_domain=entry.publisher_domain,
                    directory_properties_authorized=entry.properties_authorized,
                    federated_properties_found=0,
                    missing_in_inline=None,
                    missing_in_federated=None,
                    child_fetch_error=str(exc),
                )

        if entry.property_ids is not None:
            # Full set-diff path (adcp#4894).
            dir_ids = set(entry.property_ids)
            missing_in_inline = sorted(federated_ids - dir_ids)
            missing_in_federated = sorted(dir_ids - federated_ids)
            if not missing_in_inline and not missing_in_federated:
                return None
            return PublisherDivergence(
                publisher_domain=entry.publisher_domain,
                directory_properties_authorized=entry.properties_authorized,
                federated_properties_found=len(federated_ids),
                missing_in_inline=missing_in_inline,
                missing_in_federated=missing_in_federated,
            )

        # Count-only fallback (older directories).
        if len(federated_ids) == entry.properties_authorized:
            return None
        return PublisherDivergence(
            publisher_domain=entry.publisher_domain,
            directory_properties_authorized=entry.properties_authorized,
            federated_properties_found=len(federated_ids),
            missing_in_inline=None,
            missing_in_federated=None,
        )

    probes = await asyncio.gather(*[_probe(e) for e in collected])

    return [p for p in probes if p is not None]

Compare directory's inline resolution against per-publisher federated fetches.

For each publisher the directory lists under agent_url, fetches that publisher's own adagents.json and compares the property set against the directory's claim. Returns only publishers where the two paths disagree (or where the child fetch failed).

Always requests include=["properties"] from the directory so the full (publisher_domain, property_id) set-diff lights up on directories that support adcp#4894. Against older directories that return only properties_authorized counts, falls back to count- comparison; missing_in_inline / missing_in_federated are None in that fallback path.

Per adcp#4827 §Resolution-paths, the federated result is authoritative when the two paths disagree.

Args
-----=
agent_url
agent to check.
directory_url
AAO directory base URL (HTTPS only — same SSRF gate as :func:fetch_agent_authorizations_from_directory()).
sample_size
cap the sweep at N publishers (drawn from the first page of directory results). None opts into a full sweep across all pages — only do this for small networks. Default 200 keeps the divergence sweep bounded by default.
max_concurrency
semaphore-capped concurrent federated fetches. Default 20 — caps the burst against publisher origins.
timeout
per-request timeout (directory + child fetches).
client
optional shared httpx.AsyncClient.

Returns -----= :data:DivergenceReport (list[PublisherDivergence]). Empty list = no divergence detected. Note in count-only fallback mode, an empty list means counts agree but set-equality is not guaranteed.

def domain_matches(property_domain: str, agent_domain_pattern: str) ‑> bool
Expand source code
def domain_matches(property_domain: str, agent_domain_pattern: str) -> bool:
    """Check if domains match per AdCP rules.

    Rules:
    - Exact match always succeeds
    - 'example.com' matches www.example.com, m.example.com (common subdomains)
    - 'subdomain.example.com' matches that specific subdomain only
    - '*.example.com' matches all subdomains

    Args:
        property_domain: Domain from property
        agent_domain_pattern: Domain pattern from adagents.json

    Returns:
        True if domains match per AdCP rules
    """
    # Normalize both domains for comparison
    try:
        property_domain = _normalize_domain(property_domain)
        agent_domain_pattern = _normalize_domain(agent_domain_pattern)
    except AdagentsValidationError:
        # Invalid domain format - no match
        return False

    # Exact match
    if property_domain == agent_domain_pattern:
        return True

    # Wildcard pattern (*.example.com)
    if agent_domain_pattern.startswith("*."):
        base_domain = agent_domain_pattern[2:]
        return property_domain.endswith(f".{base_domain}")

    # Bare domain matches common subdomains (www, m)
    # If agent pattern is a bare domain (no subdomain), match www/m subdomains
    if "." in agent_domain_pattern and not agent_domain_pattern.startswith("www."):
        # Check if this looks like a bare domain (e.g., example.com)
        parts = agent_domain_pattern.split(".")
        if len(parts) == 2:  # Looks like bare domain
            common_subdomains = ["www", "m"]
            for subdomain in common_subdomains:
                if property_domain == f"{subdomain}.{agent_domain_pattern}":
                    return True

    return False

Check if domains match per AdCP rules.

Rules: - Exact match always succeeds - 'example.com' matches www.example.com, m.example.com (common subdomains) - 'subdomain.example.com' matches that specific subdomain only - '*.example.com' matches all subdomains

Args
-----=
property_domain
Domain from property
agent_domain_pattern
Domain pattern from adagents.json

Returns -----= True if domains match per AdCP rules

def encode_unreserved(raw: str) ‑> str
Expand source code
def encode_unreserved(raw: str) -> str:
    """UTF-8 encode ``raw``, escaping every byte outside RFC 3986 unreserved.

    Percent escapes use uppercase hexadecimal. Unlike
    :func:`urllib.parse.quote`, this helper does not preserve ``/``.
    """

    return "".join(
        chr(byte) if byte in _UNRESERVED_BYTES else f"%{byte:02X}" for byte in raw.encode("utf-8")
    )

UTF-8 encode raw, escaping every byte outside RFC 3986 unreserved.

Percent escapes use uppercase hexadecimal. Unlike :func:urllib.parse.quote, this helper does not preserve /.

def extract_webhook_result_data(webhook_payload: dict[str, Any]) ‑> dict[str, typing.Any] | None
Expand source code
def extract_webhook_result_data(webhook_payload: dict[str, Any]) -> dict[str, Any] | None:
    """
    Extract result data from webhook payload (MCP or A2A format).

    This utility function handles webhook payloads from both MCP and A2A protocols,
    extracting the result data regardless of the webhook format. Useful for quick
    inspection, logging, or custom webhook routing logic without requiring full
    client initialization.

    Protocol Detection:
    - A2A Task: Has "artifacts" field (terminated statuses: completed, failed, canceled, rejected)
    - A2A TaskStatusUpdateEvent: Has nested "status.message" structure (intermediate statuses)
    - MCP: Has "result" field directly

    Args:
        webhook_payload: Raw webhook dictionary from HTTP request (JSON-deserialized)

    Returns:
        dict[str, Any] containing the extracted AdCP response data, or None if no
        result is present. For A2A webhooks, unwraps data from artifacts/message parts
        structure. For MCP webhooks, returns the result field directly.

    Examples:
        Extract from MCP webhook:
        >>> mcp_payload = {
        ...     "task_id": "task_123",
        ...     "task_type": "create_media_buy",
        ...     "status": "completed",
        ...     "timestamp": "2025-01-15T10:00:00Z",
        ...     "result": {"media_buy_id": "mb_123", "buyer_ref": "ref_123", "packages": []}
        ... }
        >>> result = extract_webhook_result_data(mcp_payload)
        >>> print(result["media_buy_id"])
        mb_123

        Extract from A2A Task webhook:
        >>> a2a_task_payload = {
        ...     "id": "task_456",
        ...     "context_id": "ctx_456",
        ...     "status": {"state": "completed", "timestamp": "2025-01-15T10:00:00Z"},
        ...     "artifacts": [
        ...         {
        ...             "artifact_id": "artifact_456",
        ...             "parts": [
        ...                 {
        ...                     "data": {
        ...                         "media_buy_id": "mb_456",
        ...                         "buyer_ref": "ref_456",
        ...                         "packages": []
        ...                     }
        ...                 }
        ...             ]
        ...         }
        ...     ]
        ... }
        >>> result = extract_webhook_result_data(a2a_task_payload)
        >>> print(result["media_buy_id"])
        mb_456

        Extract from A2A TaskStatusUpdateEvent webhook:
        >>> a2a_event_payload = {
        ...     "task_id": "task_789",
        ...     "context_id": "ctx_789",
        ...     "status": {
        ...         "state": "working",
        ...         "timestamp": "2025-01-15T10:00:00Z",
        ...         "message": {
        ...             "message_id": "msg_789",
        ...             "role": "agent",
        ...             "parts": [
        ...                 {"data": {"current_step": "processing", "percentage": 50}}
        ...             ]
        ...         }
        ...     },
        ...     "final": False
        ... }
        >>> result = extract_webhook_result_data(a2a_event_payload)
        >>> print(result["percentage"])
        50

        Handle webhook with no result:
        >>> empty_payload = {"task_id": "task_000", "status": "working", "timestamp": "..."}
        >>> result = extract_webhook_result_data(empty_payload)
        >>> print(result)
        None
    """
    # Detect A2A Task format (has "artifacts" field)
    if "artifacts" in webhook_payload:
        # Extract from task.artifacts[].parts[]
        artifacts = webhook_payload.get("artifacts", [])
        if not artifacts:
            return None

        # Use last artifact (most recent)
        target_artifact = artifacts[-1]
        parts = target_artifact.get("parts", [])
        if not parts:
            return None

        # Find DataPart (skip TextPart)
        for part in parts:
            # Check if this part has "data" field (DataPart)
            if "data" in part:
                data = part["data"]
                # Unwrap {"response": {...}} wrapper if present (A2A convention)
                if isinstance(data, dict) and "response" in data and len(data) == 1:
                    return cast(dict[str, Any], data["response"])
                return cast(dict[str, Any], data)

        return None

    # Detect A2A TaskStatusUpdateEvent format (has nested "status.message")
    status = webhook_payload.get("status")
    if isinstance(status, dict):
        message = status.get("message")
        if isinstance(message, dict):
            # Extract from status.message.parts[]
            parts = message.get("parts", [])
            if not parts:
                return None

            # Find DataPart
            for part in parts:
                if "data" in part:
                    data = part["data"]
                    # Unwrap {"response": {...}} wrapper if present
                    if isinstance(data, dict) and "response" in data and len(data) == 1:
                        return cast(dict[str, Any], data["response"])
                    return cast(dict[str, Any], data)

            return None

    # MCP format: result field directly
    return cast(dict[str, Any] | None, webhook_payload.get("result"))

Extract result data from webhook payload (MCP or A2A format).

This utility function handles webhook payloads from both MCP and A2A protocols, extracting the result data regardless of the webhook format. Useful for quick inspection, logging, or custom webhook routing logic without requiring full client initialization.

Protocol Detection: - A2A Task: Has "artifacts" field (terminated statuses: completed, failed, canceled, rejected) - A2A TaskStatusUpdateEvent: Has nested "status.message" structure (intermediate statuses) - MCP: Has "result" field directly

Args
-----=
webhook_payload
Raw webhook dictionary from HTTP request (JSON-deserialized)

Returns -----= dict[str, Any] containing the extracted AdCP response data, or None if no result is present. For A2A webhooks, unwraps data from artifacts/message parts structure. For MCP webhooks, returns the result field directly.

Examples -----= Extract from MCP webhook:

>>> mcp_payload = {
...     "task_id": "task_123",
...     "task_type": "create_media_buy",
...     "status": "completed",
...     "timestamp": "2025-01-15T10:00:00Z",
...     "result": {"media_buy_id": "mb_123", "buyer_ref": "ref_123", "packages": []}
... }
>>> result = extract_webhook_result_data(mcp_payload)
>>> print(result["media_buy_id"])
mb_123

Extract from A2A Task webhook:

>>> a2a_task_payload = {
...     "id": "task_456",
...     "context_id": "ctx_456",
...     "status": {"state": "completed", "timestamp": "2025-01-15T10:00:00Z"},
...     "artifacts": [
...         {
...             "artifact_id": "artifact_456",
...             "parts": [
...                 {
...                     "data": {
...                         "media_buy_id": "mb_456",
...                         "buyer_ref": "ref_456",
...                         "packages": []
...                     }
...                 }
...             ]
...         }
...     ]
... }
>>> result = extract_webhook_result_data(a2a_task_payload)
>>> print(result["media_buy_id"])
mb_456

Extract from A2A TaskStatusUpdateEvent webhook:

>>> a2a_event_payload = {
...     "task_id": "task_789",
...     "context_id": "ctx_789",
...     "status": {
...         "state": "working",
...         "timestamp": "2025-01-15T10:00:00Z",
...         "message": {
...             "message_id": "msg_789",
...             "role": "agent",
...             "parts": [
...                 {"data": {"current_step": "processing", "percentage": 50}}
...             ]
...         }
...     },
...     "final": False
... }
>>> result = extract_webhook_result_data(a2a_event_payload)
>>> print(result["percentage"])
50

Handle webhook with no result:

>>> empty_payload = {"task_id": "task_000", "status": "working", "timestamp": "..."}
>>> result = extract_webhook_result_data(empty_payload)
>>> print(result)
None
async def fetch_adagents(publisher_domain: str,
timeout: float = 10.0,
user_agent: str = 'AdCP-Client/1.0',
client: httpx.AsyncClient | None = None) ‑> dict[str, typing.Any]
Expand source code
async def fetch_adagents(
    publisher_domain: str,
    timeout: float = 10.0,
    user_agent: str = "AdCP-Client/1.0",
    client: httpx.AsyncClient | None = None,
) -> dict[str, Any]:
    """Fetch and parse adagents.json from publisher domain.

    Discovery order:

    1. ``https://{publisher}/.well-known/adagents.json`` (direct).
    2. ``authoritative_location`` redirect, if the direct response is a
       URL reference.
    3. RFC 4175 ads.txt MANAGERDOMAIN fallback, on direct 404 only:
       fetches ``https://{publisher}/ads.txt`` for a
       ``MANAGERDOMAIN=`` directive and, if present, tries
       ``https://{manager}/.well-known/adagents.json``.

    The fallback is one-hop only. If the manager domain also 404s,
    this raises :class:`AdagentsNotFoundError` for the original
    publisher — not a silent pass.

    Args:
        publisher_domain: Domain hosting the adagents.json file.
        timeout: Request timeout in seconds.
        user_agent: User-Agent header for HTTP request.
        client: Optional httpx.AsyncClient for connection pooling.
            If provided, caller is responsible for client lifecycle.
            If None, a new client is created for this request.

    Returns:
        Parsed adagents.json data (resolved via authoritative_location
        or ads.txt MANAGERDOMAIN if applicable).

    Raises:
        AdagentsNotFoundError: If adagents.json was not found via any
            discovery path.
        AdagentsAccessBlockedError: If the publisher's CDN returns HTTP
            403 with ``cf-mitigated: challenge`` (Cloudflare bot-management
            block). Subclass of ``AdagentsValidationError``.
        AdagentsValidationError: If JSON is invalid, malformed, or
            redirects exceed maximum depth or form a loop.
        AdagentsTimeoutError: If request times out.

    Notes:
        For production use with multiple requests, pass a shared
        httpx.AsyncClient to enable connection pooling.

        Callers who need to know which discovery path produced the
        data (direct, authoritative_location, or ads_txt_managerdomain)
        should call :func:`validate_adagents_domain` instead.

        ``fetch_adagents`` performs only minimal structural checks. To
        report per-entry schema violations (e.g., bare entries missing
        ``authorization_type``) without raising, pass the returned data
        to :func:`validate_adagents_structure`.
    """
    publisher_domain = _validate_publisher_domain(publisher_domain)

    try:
        data, *_ = await _resolve_direct(publisher_domain, timeout, user_agent, client)
        return data
    except AdagentsNotFoundError:
        manager_data = await _try_managerdomain_fallback(
            publisher_domain, timeout, user_agent, client
        )
        if manager_data is not None:
            return manager_data
        raise

Fetch and parse adagents.json from publisher domain.

Discovery order:

  1. https://{publisher}/.well-known/adagents.json (direct).
  2. authoritative_location redirect, if the direct response is a URL reference.
  3. RFC 4175 ads.txt MANAGERDOMAIN fallback, on direct 404 only: fetches https://{publisher}/ads.txt for a MANAGERDOMAIN= directive and, if present, tries https://{manager}/.well-known/adagents.json.

The fallback is one-hop only. If the manager domain also 404s, this raises :class:AdagentsNotFoundError for the original publisher — not a silent pass.

Args
-----=
publisher_domain
Domain hosting the adagents.json file.
timeout
Request timeout in seconds.
user_agent
User-Agent header for HTTP request.
client
Optional httpx.AsyncClient for connection pooling. If provided, caller is responsible for client lifecycle. If None, a new client is created for this request.

Returns -----= Parsed adagents.json data (resolved via authoritative_location or ads.txt MANAGERDOMAIN if applicable).

Raises
-----=
AdagentsNotFoundError
If adagents.json was not found via any discovery path.
AdagentsAccessBlockedError
If the publisher's CDN returns HTTP 403 with cf-mitigated: challenge (Cloudflare bot-management block). Subclass of AdagentsValidationError.
AdagentsValidationError
If JSON is invalid, malformed, or redirects exceed maximum depth or form a loop.
AdagentsTimeoutError
If request times out.

Notes -----= For production use with multiple requests, pass a shared httpx.AsyncClient to enable connection pooling.

Callers who need to know which discovery path produced the data (direct, authoritative_location, or ads_txt_managerdomain) should call :func:validate_adagents_domain() instead.

fetch_adagents() performs only minimal structural checks. To report per-entry schema violations (e.g., bare entries missing authorization_type) without raising, pass the returned data to :func:validate_adagents_structure().

async def fetch_adagents_with_cache(publisher_domain: str,
cache_entry: AdagentsCacheEntry | None = None,
timeout: float = 10.0,
user_agent: str = 'AdCP-Client/1.0',
client: httpx.AsyncClient | None = None) ‑> AdagentsFetchResult
Expand source code
async def fetch_adagents_with_cache(
    publisher_domain: str,
    cache_entry: AdagentsCacheEntry | None = None,
    timeout: float = 10.0,
    user_agent: str = "AdCP-Client/1.0",
    client: httpx.AsyncClient | None = None,
) -> AdagentsFetchResult:
    """Fetch with conditional refresh — returns body plus refreshed validators.

    Pass the previous fetch's :class:`AdagentsCacheEntry` to send
    ``If-None-Match`` / ``If-Modified-Since`` on the next fetch. A 304
    from the publisher is treated as a successful refresh: the cached
    ``body`` is returned with ``not_modified=True``, satisfying the
    7-day cache window described in adcp#4504.

    The first hop (``/.well-known/adagents.json``) is capped at 5 MiB;
    a dereferenced ``authoritative_location`` file is capped at 20 MiB.
    Both caps fail closed — oversized responses raise
    :class:`AdagentsValidationError` rather than truncate.

    Does NOT perform the ads.txt ``managerdomain`` fallback; the
    fallback is best-effort discovery, not cache-aware refresh, and
    bypassing it on 304 keeps the path simple. Callers that need both
    behaviors should compose this helper with
    :func:`validate_adagents_domain`.
    """
    publisher_domain = _validate_publisher_domain(publisher_domain)
    data, discovery, etag, last_modified, not_modified = await _resolve_direct(
        publisher_domain, timeout, user_agent, client, cache_entry=cache_entry
    )
    return AdagentsFetchResult(
        data=data,
        discovery_method=discovery,
        etag=etag,
        last_modified=last_modified,
        not_modified=not_modified,
    )

Fetch with conditional refresh — returns body plus refreshed validators.

Pass the previous fetch's :class:AdagentsCacheEntry to send If-None-Match / If-Modified-Since on the next fetch. A 304 from the publisher is treated as a successful refresh: the cached body is returned with not_modified=True, satisfying the 7-day cache window described in adcp#4504.

The first hop (/.well-known/adagents.json) is capped at 5 MiB; a dereferenced authoritative_location file is capped at 20 MiB. Both caps fail closed — oversized responses raise :class:AdagentsValidationError rather than truncate.

Does NOT perform the ads.txt managerdomain fallback; the fallback is best-effort discovery, not cache-aware refresh, and bypassing it on 304 keeps the path simple. Callers that need both behaviors should compose this helper with :func:validate_adagents_domain().

async def fetch_agent_authorizations(agent_url: str,
publisher_domains: list[str],
timeout: float = 10.0,
client: httpx.AsyncClient | None = None) ‑> dict[str, AuthorizationContext]
Expand source code
async def fetch_agent_authorizations(
    agent_url: str,
    publisher_domains: list[str],
    timeout: float = 10.0,
    client: httpx.AsyncClient | None = None,
) -> dict[str, AuthorizationContext]:
    """Fetch authorization contexts by checking publisher adagents.json files.

    This function discovers what publishers have authorized your agent by fetching
    their adagents.json files from the .well-known directory and extracting the
    properties your agent can access.

    This is the "pull" approach - you query publishers to see if they've authorized you.

    Args:
        agent_url: URL of your sales agent
        publisher_domains: List of publisher domains to check (e.g., ["nytimes.com", "wsj.com"])
        timeout: Request timeout in seconds for each fetch
        client: Optional httpx.AsyncClient for connection pooling

    Returns:
        Dictionary mapping publisher domain to AuthorizationContext.
        Only includes domains where the agent is authorized.

    Example:
        >>> # "Pull" approach - check what publishers have authorized you
        >>> contexts = await fetch_agent_authorizations(
        ...     "https://our-sales-agent.com",
        ...     ["nytimes.com", "wsj.com", "cnn.com"]
        ... )
        >>> for domain, ctx in contexts.items():
        ...     print(f"{domain}:")
        ...     print(f"  Property IDs: {ctx.property_ids}")
        ...     print(f"  Tags: {ctx.property_tags}")

    Notes:
        - Silently skips domains where adagents.json is not found or invalid
        - Only returns domains where the agent is explicitly authorized
        - For production use with many domains, pass a shared httpx.AsyncClient
          to enable connection pooling
    """
    import asyncio

    # Create tasks to fetch all adagents.json files in parallel
    async def fetch_authorization_for_domain(
        domain: str,
    ) -> tuple[str, AuthorizationContext | None]:
        """Fetch authorization context for a single domain."""
        try:
            adagents_data = await fetch_adagents(domain, timeout=timeout, client=client)

            # Check if agent is authorized
            if not verify_agent_authorization(adagents_data, agent_url):
                return (domain, None)

            # Get properties for this agent
            properties = get_properties_by_agent(adagents_data, agent_url)

            # Create authorization context
            return (domain, AuthorizationContext(properties))

        except (AdagentsNotFoundError, AdagentsValidationError, AdagentsTimeoutError):
            # Silently skip domains with missing or invalid adagents.json.
            # AdagentsAccessBlockedError (AdagentsValidationError subclass) is
            # intentionally swallowed: a bot-blocked domain is treated as
            # authorization-unavailable, same as a missing file.
            return (domain, None)

    # Fetch all domains in parallel
    tasks = [fetch_authorization_for_domain(domain) for domain in publisher_domains]
    results = await asyncio.gather(*tasks)

    # Build result dictionary, filtering out None values
    return {domain: ctx for domain, ctx in results if ctx is not None}

Fetch authorization contexts by checking publisher adagents.json files.

This function discovers what publishers have authorized your agent by fetching their adagents.json files from the .well-known directory and extracting the properties your agent can access.

This is the "pull" approach - you query publishers to see if they've authorized you.

Args
-----=
agent_url
URL of your sales agent
publisher_domains
List of publisher domains to check (e.g., ["nytimes.com", "wsj.com"])
timeout
Request timeout in seconds for each fetch
client
Optional httpx.AsyncClient for connection pooling

Returns -----= Dictionary mapping publisher domain to AuthorizationContext. Only includes domains where the agent is authorized.

Example -----=

>>> # "Pull" approach - check what publishers have authorized you
>>> contexts = await fetch_agent_authorizations(
...     "https://our-sales-agent.com",
...     ["nytimes.com", "wsj.com", "cnn.com"]
... )
>>> for domain, ctx in contexts.items():
...     print(f"{domain}:")
...     print(f"  Property IDs: {ctx.property_ids}")
...     print(f"  Tags: {ctx.property_tags}")

Notes -----= - Silently skips domains where adagents.json is not found or invalid - Only returns domains where the agent is explicitly authorized - For production use with many domains, pass a shared httpx.AsyncClient to enable connection pooling

async def fetch_agent_authorizations_from_directory(agent_url: str,
*,
directory_url: str,
since: str | None = None,
cursor: str | None = None,
include: list[str] | None = None,
timeout: float = 10.0,
client: httpx.AsyncClient | None = None) ‑> AgentAuthorizationsDirectoryResult
Expand source code
async def fetch_agent_authorizations_from_directory(
    agent_url: str,
    *,
    directory_url: str,
    since: str | None = None,
    cursor: str | None = None,
    include: list[str] | None = None,
    timeout: float = 10.0,
    client: httpx.AsyncClient | None = None,
) -> AgentAuthorizationsDirectoryResult:
    """Query an AAO directory for publishers that authorize ``agent_url``.

    Calls ``GET {directory_url}/v1/agents/{agent_url}/publishers`` per the
    AAO inverse-lookup contract (adcp#4823 / #4828) and returns the parsed
    response. The directory's answer is *discovery*, not authorization:
    callers should still verify each returned ``publisher_domain`` via
    :func:`fetch_adagents` before treating an edge as trusted.

    Args:
        agent_url: The agent whose publisher authorizations are being
            queried. Passed verbatim in the path; the directory echoes
            back a canonicalized form on the response.
        directory_url: HTTPS base URL of the AAO directory
            (e.g. ``"https://aao.example.com"``). The ``/v1/agents/...``
            path is appended; pass the directory's root, not a
            request-specific path.
        since: Optional RFC 3339 timestamp from a prior
            ``directory_indexed_at`` — passed through as ``?since=...``
            to limit the result to edges that changed since that point.
        cursor: Optional opaque pagination cursor from a prior response's
            ``next_cursor`` — passed through as ``?cursor=...`` to fetch
            the next page.
        include: Optional list of expansion keys per the AAO directory
            API spec (adcp#4894). Each value is emitted as a separate
            ``?include=<value>`` query parameter (repeated-key form, not
            comma-joined). Pass ``["properties"]`` against directories
            that support it to receive per-publisher ``property_ids[]``
            on each row, enabling full set-diff against the publisher's
            own adagents.json. Directories that don't support a given
            expansion key simply omit the corresponding fields from the
            response; callers should treat absence as count-only mode.
        timeout: Request timeout in seconds.
        client: Optional shared ``httpx.AsyncClient`` for connection
            pooling. Caller owns the client lifecycle.

    Returns:
        :class:`AgentAuthorizationsDirectoryResult`. On 404 from the
        directory the function returns a result with ``publishers=[]``
        and ``directory_indexed_at=None`` — directories MUST be allowed
        to answer "I do not index this agent" without callers needing
        to branch on exception type.

    Raises:
        AdagentsValidationError: If ``directory_url`` is malformed, the
            response status is non-200/non-404, the body is not valid
            JSON, or the body does not match the directory result schema.
        AdagentsTimeoutError: If the request times out.

    Notes:
        - ``directory_url`` is gated through the same SSRF protection
          (HTTPS only, DNS pre-check, private/reserved address ban) as
          publisher-side fetches.
        - Response bodies are capped at 5 MiB. Bulk responses paginate
          via ``next_cursor``; pass that value as ``cursor`` on the next
          call.
    """
    if not isinstance(agent_url, str) or not agent_url:
        raise AdagentsValidationError("agent_url must be a non-empty string")
    if not isinstance(directory_url, str) or not directory_url:
        raise AdagentsValidationError("directory_url must be a non-empty string")

    base = directory_url.rstrip("/")
    if not base.startswith("https://"):
        raise AdagentsValidationError(f"directory_url must be an HTTPS URL, got: {directory_url!r}")
    _validate_redirect_url(f"{base}/v1/agents/_/publishers")

    request_url = f"{base}/v1/agents/{quote(agent_url, safe='')}/publishers"
    query_pairs: list[tuple[str, str]] = []
    if since is not None:
        query_pairs.append(("since", since))
    if cursor is not None:
        query_pairs.append(("cursor", cursor))
    if include:
        # Repeated-key form per docs/aao/directory-api.mdx (style: form,
        # explode: true). Comma-joined NOT accepted by spec-conformant
        # directories.
        for value in include:
            query_pairs.append(("include", value))
    if query_pairs:
        query_string = "&".join(f"{quote(k, safe='')}={quote(v, safe='')}" for k, v in query_pairs)
        request_url = f"{request_url}?{query_string}"

    parsed = urlparse(request_url)
    await _dns_validate_host(
        parsed.hostname or "", parsed.port or (443 if parsed.scheme == "https" else 80)
    )

    headers = {"User-Agent": "AdCP-Client/1.0", "Accept": "application/json"}

    # SDK-owned client is pinned to the validated IP (see _fetch_adagents_url).
    # A failed resolve/SSRF check raises AdagentsValidationError, which
    # propagates past the httpx handlers below — the correct fail-closed
    # outcome (we do not convert it into an empty result).
    try:
        if client is not None:
            body, status_code, _ = await _stream_capped(
                client, request_url, headers, timeout, MAX_DIRECTORY_PAGE_BYTES
            )
        else:
            async with _owned_pinned_client(request_url, timeout) as new_client:
                body, status_code, _ = await _stream_capped(
                    new_client, request_url, headers, timeout, MAX_DIRECTORY_PAGE_BYTES
                )
    except httpx.TimeoutException as e:
        raise AdagentsTimeoutError(parsed.netloc, timeout) from e
    except httpx.RequestError as e:
        raise AdagentsValidationError(f"Failed to fetch agent-publishers directory: {e}") from e

    if status_code == 404:
        # Per adcp#4828, a directory that has not indexed this agent
        # answers 404. Surface as an empty result so callers don't need
        # to special-case the exception path for "no edges" — the
        # protocol is intentionally permissive here.
        return AgentAuthorizationsDirectoryResult(
            agent_url=agent_url,
            directory_indexed_at=None,
            publishers=[],
            next_cursor=None,
        )

    if status_code != 200:
        raise AdagentsValidationError(f"Agent-publishers directory returned HTTP {status_code}")

    try:
        data = json.loads(body)
    except json.JSONDecodeError as e:
        raise AdagentsValidationError(
            f"Invalid JSON in agent-publishers directory response: {str(e)[:200]}"
        ) from e

    try:
        return AgentAuthorizationsDirectoryResult.model_validate(data)
    except Exception as e:  # pydantic.ValidationError + any coercion failure
        raise AdagentsValidationError(
            f"Agent-publishers directory response failed schema validation: {e}"
        ) from e

Query an AAO directory for publishers that authorize agent_url.

Calls GET {directory_url}/v1/agents/{agent_url}/publishers per the AAO inverse-lookup contract (adcp#4823 / #4828) and returns the parsed response. The directory's answer is discovery, not authorization: callers should still verify each returned publisher_domain via :func:fetch_adagents() before treating an edge as trusted.

Args
-----=
agent_url
The agent whose publisher authorizations are being queried. Passed verbatim in the path; the directory echoes back a canonicalized form on the response.
directory_url
HTTPS base URL of the AAO directory (e.g. "https://aao.example.com"). The /v1/agents/... path is appended; pass the directory's root, not a request-specific path.
since
Optional RFC 3339 timestamp from a prior directory_indexed_at — passed through as ?since=... to limit the result to edges that changed since that point.
cursor
Optional opaque pagination cursor from a prior response's next_cursor — passed through as ?cursor=... to fetch the next page.
include
Optional list of expansion keys per the AAO directory API spec (adcp#4894). Each value is emitted as a separate ?include=<value> query parameter (repeated-key form, not comma-joined). Pass ["properties"] against directories that support it to receive per-publisher property_ids[] on each row, enabling full set-diff against the publisher's own adagents.json. Directories that don't support a given expansion key simply omit the corresponding fields from the response; callers should treat absence as count-only mode.
timeout
Request timeout in seconds.
client
Optional shared httpx.AsyncClient for connection pooling. Caller owns the client lifecycle.

Returns -----= :class:AgentAuthorizationsDirectoryResult. On 404 from the directory the function returns a result with publishers=[] and directory_indexed_at=None — directories MUST be allowed to answer "I do not index this agent" without callers needing to branch on exception type.

Raises
-----=
AdagentsValidationError
If directory_url is malformed, the response status is non-200/non-404, the body is not valid JSON, or the body does not match the directory result schema.
AdagentsTimeoutError
If the request times out.

Notes -----= - directory_url is gated through the same SSRF protection (HTTPS only, DNS pre-check, private/reserved address ban) as publisher-side fetches. - Response bodies are capped at 5 MiB. Bulk responses paginate via next_cursor; pass that value as cursor on the next call.

def filter_revoked_selectors(selectors: list[dict[str, Any]], revoked_domains: set[str]) ‑> list[dict[str, typing.Any]]
Expand source code
def filter_revoked_selectors(
    selectors: list[dict[str, Any]],
    revoked_domains: set[str],
) -> list[dict[str, Any]]:
    """Strip selectors whose ``publisher_domain`` is revoked.

    Apply this AFTER the compact-form fan-out so each remaining selector
    addresses exactly one publisher, then drop any whose domain is in
    ``revoked_domains``. Revocation takes precedence over every other
    listing of that domain in the file (selectors, top-level properties,
    etc.) per adcp#4504.
    """
    if not revoked_domains:
        return selectors
    return [s for s in selectors if s.get("publisher_domain") not in revoked_domains]

Strip selectors whose publisher_domain is revoked.

Apply this AFTER the compact-form fan-out so each remaining selector addresses exactly one publisher, then drop any whose domain is in revoked_domains. Revocation takes precedence over every other listing of that domain in the file (selectors, top-level properties, etc.) per adcp#4504.

def format_is_supported(requested: str | FormatId | Mapping[str, Any],
supported: str | FormatId | Mapping[str, Any],
*,
default_agent_url: str = 'https://creative.adcontextprotocol.org') ‑> bool
Expand source code
def format_is_supported(
    requested: str | FormatId | Mapping[str, Any],
    supported: str | FormatId | Mapping[str, Any],
    *,
    default_agent_url: str = CANONICAL_CREATIVE_AGENT_URL,
) -> bool:
    """Return true when ``requested`` is acceptable for ``supported``.

    This is intentionally stricter than :func:`formats_are_equivalent`.
    A broad supported format such as ``display_image`` accepts a specific
    request such as ``display_image`` 300x250, but a fixed supported product
    format requires the request to provide and match every fixed parameter
    (``width``, ``height``, and ``duration_ms``).
    """
    req = upgrade_legacy_format_id(requested, default_agent_url=default_agent_url)
    sup = upgrade_legacy_format_id(supported, default_agent_url=default_agent_url)
    if not formats_are_equivalent(req, sup, default_agent_url=default_agent_url):
        return False

    for field in ("width", "height", "duration_ms"):
        supported_value = getattr(sup, field)
        if supported_value is None:
            continue
        if getattr(req, field) != supported_value:
            return False
    return True

Return true when requested is acceptable for supported.

This is intentionally stricter than :func:formats_are_equivalent(). A broad supported format such as display_image accepts a specific request such as display_image 300x250, but a fixed supported product format requires the request to provide and match every fixed parameter (width, height, and duration_ms).

def formats_are_equivalent(a: str | FormatId | Mapping[str, Any],
b: str | FormatId | Mapping[str, Any],
*,
default_agent_url: str = 'https://creative.adcontextprotocol.org') ‑> bool
Expand source code
def formats_are_equivalent(
    a: str | FormatId | Mapping[str, Any],
    b: str | FormatId | Mapping[str, Any],
    *,
    default_agent_url: str = CANONICAL_CREATIVE_AGENT_URL,
) -> bool:
    """Return true when two format IDs identify the same canonical family.

    Both inputs are first passed through :func:`upgrade_legacy_format_id`.
    Declared parameters must not conflict, but an omitted parameter on either
    side is treated as unspecified rather than a mismatch. Use
    :func:`format_is_supported` for product/capability gating where a
    supported fixed size or duration requires the request to state that value.
    """
    left = upgrade_legacy_format_id(a, default_agent_url=default_agent_url)
    right = upgrade_legacy_format_id(b, default_agent_url=default_agent_url)
    if canonicalize_agent_url(left.agent_url) != canonicalize_agent_url(right.agent_url):
        return False
    if left.id != right.id:
        return False

    for field in ("width", "height", "duration_ms"):
        left_value = getattr(left, field)
        right_value = getattr(right, field)
        if left_value is not None and right_value is not None and left_value != right_value:
            return False
    return True

Return true when two format IDs identify the same canonical family.

Both inputs are first passed through :func:upgrade_legacy_format_id(). Declared parameters must not conflict, but an omitted parameter on either side is treated as unspecified rather than a mismatch. Use :func:format_is_supported() for product/capability gating where a supported fixed size or duration requires the request to state that value.

def generate_webhook_challenge_value() ‑> str
Expand source code
def generate_webhook_challenge_value() -> str:
    """Generate an opaque random value for a proof-of-control challenge."""

    return f"wch_{secrets.token_urlsafe(32)}"

Generate an opaque random value for a proof-of-control challenge.

def generate_webhook_idempotency_key() ‑> str
Expand source code
def generate_webhook_idempotency_key() -> str:
    """Generate a cryptographically random idempotency_key for a webhook event.

    Returns a UUID v4 prefixed with ``whk_`` — matches the example format in
    ``webhooks.mdx`` and stays within the spec's length + charset bounds
    (``^[A-Za-z0-9_.:-]{16,255}$``).

    Publishers generate this once per delivery and reuse it for exact retries.
    A re-emission of the same logical event uses a new delivery key while
    retaining its stable ``notification_id``.
    """
    return f"whk_{uuid.uuid4()}"

Generate a cryptographically random idempotency_key for a webhook event.

Returns a UUID v4 prefixed with whk_ — matches the example format in webhooks.mdx and stays within the spec's length + charset bounds (^[A-Za-z0-9_.:-]{16,255}$).

Publishers generate this once per delivery and reuse it for exact retries. A re-emission of the same logical event uses a new delivery key while retaining its stable notification_id.

def get_adcp_signed_headers_for_webhook(headers: dict[str, Any],
secret: str,
timestamp: str | int | None,
payload: dict[str, Any] | AdCPBaseModel) ‑> dict[str, typing.Any]
Expand source code
def get_adcp_signed_headers_for_webhook(
    headers: dict[str, Any],
    secret: str,
    timestamp: str | int | None,
    payload: dict[str, Any] | AdCPBaseModel,
) -> dict[str, Any]:
    """
    Generate AdCP-compliant signed headers for webhook delivery.

    This function creates a cryptographic signature that proves the webhook
    came from an authorized agent and protects against replay attacks by
    including a timestamp in the signed message.

    The function adds two headers to the provided headers dict:
    - X-AdCP-Signature: HMAC-SHA256 signature in format "sha256=<hex_digest>"
    - X-AdCP-Timestamp: Unix timestamp in seconds

    The signing algorithm:
    1. Constructs message as "{timestamp}.{json_payload}"
    2. JSON-serializes payload with default separators (matches wire format from json= kwarg)
    3. UTF-8 encodes the message
    4. HMAC-SHA256 signs with the shared secret
    5. Hex-encodes and prefixes with "sha256="

    Args:
        headers: Existing headers dictionary to add signature headers to
        secret: Shared secret key for HMAC signing
        timestamp: Unix timestamp in seconds (str or int). If None, uses current time.
        payload: Webhook payload (dict or Pydantic model - will be JSON-serialized)

    Returns:
        The modified headers dictionary with signature headers added

    Examples:
        Sign and send an MCP webhook:
        >>> import time
        >>> from adcp.webhooks import create_mcp_webhook_payload
        >>> from adcp.webhooks import get_adcp_signed_headers_for_webhook
        >>>
        >>> payload = create_mcp_webhook_payload(
        ...     task_id="task_123",
        ...     operation_id="op_123",
        ...     status="completed",
        ...     task_type="create_media_buy",
        ...     result={"media_buy_id": "mb_1"},
        ... )
        >>> headers = {"Content-Type": "application/json"}
        >>> signed_headers = get_adcp_signed_headers_for_webhook(
        ...     headers, secret="my-webhook-secret", timestamp=str(int(time.time())),
        ...     payload=payload,
        ... )
        >>>
        >>> # Send webhook with signed headers
        >>> import httpx
        >>> response = await httpx.post(
        ...     webhook_url,
        ...     json=payload,
        ...     headers=signed_headers
        ... )

        Headers will contain:
        >>> print(signed_headers)
        {
            "Content-Type": "application/json",
            "X-AdCP-Signature": "sha256=a1b2c3...",
            "X-AdCP-Timestamp": "1773185740"
        }
    """
    signature_headers, _body_bytes = _compute_legacy_signature(
        secret=secret, timestamp=timestamp, payload=payload
    )
    headers.update(signature_headers)
    return headers

Generate AdCP-compliant signed headers for webhook delivery.

This function creates a cryptographic signature that proves the webhook came from an authorized agent and protects against replay attacks by including a timestamp in the signed message.

The function adds two headers to the provided headers dict: - X-AdCP-Signature: HMAC-SHA256 signature in format "sha256=" - X-AdCP-Timestamp: Unix timestamp in seconds

The signing algorithm: 1. Constructs message as "{timestamp}.{json_payload}" 2. JSON-serializes payload with default separators (matches wire format from json= kwarg) 3. UTF-8 encodes the message 4. HMAC-SHA256 signs with the shared secret 5. Hex-encodes and prefixes with "sha256="

Args
-----=
headers
Existing headers dictionary to add signature headers to
secret
Shared secret key for HMAC signing
timestamp
Unix timestamp in seconds (str or int). If None, uses current time.
payload
Webhook payload (dict or Pydantic model - will be JSON-serialized)

Returns -----= The modified headers dictionary with signature headers added

Examples -----= Sign and send an MCP webhook:

>>> import time
>>> from adcp.webhooks import create_mcp_webhook_payload
>>> from adcp.webhooks import get_adcp_signed_headers_for_webhook
>>>
>>> payload = create_mcp_webhook_payload(
...     task_id="task_123",
...     operation_id="op_123",
...     status="completed",
...     task_type="create_media_buy",
...     result={"media_buy_id": "mb_1"},
... )
>>> headers = {"Content-Type": "application/json"}
>>> signed_headers = get_adcp_signed_headers_for_webhook(
...     headers, secret="my-webhook-secret", timestamp=str(int(time.time())),
...     payload=payload,
... )
>>>
>>> # Send webhook with signed headers
>>> import httpx
>>> response = await httpx.post(
...     webhook_url,
...     json=payload,
...     headers=signed_headers
... )

Headers will contain:

>>> print(signed_headers)
{
    "Content-Type": "application/json",
    "X-AdCP-Signature": "sha256=a1b2c3...",
    "X-AdCP-Timestamp": "1773185740"
}
def get_adcp_version() ‑> str
Expand source code
def get_adcp_version() -> str:
    """Return the AdCP *spec* version (legacy name).

    .. deprecated:: 4.1
        Kept for backwards compatibility with pre-4.1 callers. Prefer
        :func:`get_adcp_spec_version` (spec version) or
        :func:`get_adcp_sdk_version` / :attr:`adcp.__version__` (SDK
        package version) — the split disambiguates what the caller
        actually wants at the call site.
    """
    import warnings

    warnings.warn(
        "get_adcp_version() is deprecated; use get_adcp_spec_version() "
        "for the AdCP spec version or get_adcp_sdk_version() / "
        "adcp.__version__ for the SDK package version.",
        DeprecationWarning,
        stacklevel=2,
    )
    return get_adcp_spec_version()

Return the AdCP spec version (legacy name).

Deprecated since version: 4.1

Kept for backwards compatibility with pre-4.1 callers. Prefer :func:get_adcp_spec_version (spec version) or :func:get_adcp_sdk_version / :attr:adcp.__version__ (SDK package version) — the split disambiguates what the caller actually wants at the call site.

def get_all_properties(adagents_data: dict[str, Any]) ‑> list[dict[str, typing.Any]]
Expand source code
def get_all_properties(adagents_data: dict[str, Any]) -> list[dict[str, Any]]:
    """Extract all properties from adagents.json data.

    Handles all authorization types: inline_properties, property_ids,
    property_tags, and publisher_properties.

    For ``publisher_properties`` selectors whose target ``publisher_domain``
    is NOT present inline in this file's top-level ``properties[]`` array,
    this function returns no properties for that selector. Federated
    fallback (fetching the child publisher's own adagents.json to resolve
    the selector remotely) is out of scope here and lives in
    :func:`fetch_agent_authorizations_from_directory` and
    :func:`detect_publisher_properties_divergence` from companion PR #752.
    Wire-only authorization checks that assume federated resolution will
    under-authorize against managed-network parent files that only inline
    a subset of their child domains.

    Args:
        adagents_data: Parsed adagents.json data

    Returns:
        List of all properties across all authorized agents, with agent_url added

    Raises:
        AdagentsValidationError: If adagents_data is malformed
    """
    if not isinstance(adagents_data, dict):
        raise AdagentsValidationError("adagents_data must be a dictionary")

    authorized_agents = adagents_data.get("authorized_agents")
    if not isinstance(authorized_agents, list):
        raise AdagentsValidationError("adagents.json must have 'authorized_agents' array")

    top_level_properties = adagents_data.get("properties", [])
    if not isinstance(top_level_properties, list):
        top_level_properties = []

    revoked = _get_revoked_publisher_domains(adagents_data)
    revoked_top_level = [
        p
        for p in top_level_properties
        if not (
            isinstance(p, dict)
            and isinstance(p.get("publisher_domain"), str)
            and p["publisher_domain"] in revoked
        )
    ]

    # Build the domain index once per file — _resolve_agent_properties is
    # called per-agent, and at cafemedia scale (thousands of properties ×
    # multiple agents) rebuilding it inside each call is O(agents × N).
    domain_index = _build_domain_index(revoked_top_level)

    properties = []
    for agent in authorized_agents:
        if not isinstance(agent, dict):
            continue

        agent_url = agent.get("url", "")
        if not agent_url:
            continue

        # revoked_top_level pre-filters revoked domains from the per-domain
        # index, so inline resolution honors revocation transparently.
        agent_properties = _resolve_agent_properties(agent, revoked_top_level, domain_index)

        for prop in agent_properties:
            prop_with_agent = {**prop, "agent_url": agent_url}
            properties.append(prop_with_agent)

    return properties

Extract all properties from adagents.json data.

Handles all authorization types: inline_properties, property_ids, property_tags, and publisher_properties.

For publisher_properties selectors whose target publisher_domain is NOT present inline in this file's top-level properties[] array, this function returns no properties for that selector. Federated fallback (fetching the child publisher's own adagents.json to resolve the selector remotely) is out of scope here and lives in :func:fetch_agent_authorizations_from_directory() and :func:detect_publisher_properties_divergence() from companion PR #752. Wire-only authorization checks that assume federated resolution will under-authorize against managed-network parent files that only inline a subset of their child domains.

Args
-----=
adagents_data
Parsed adagents.json data

Returns -----= List of all properties across all authorized agents, with agent_url added

Raises
-----=
AdagentsValidationError
If adagents_data is malformed
def get_all_tags(adagents_data: dict[str, Any]) ‑> set[str]
Expand source code
def get_all_tags(adagents_data: dict[str, Any]) -> set[str]:
    """Extract all unique tags from properties in adagents.json data.

    Args:
        adagents_data: Parsed adagents.json data

    Returns:
        Set of all unique tags across all properties

    Raises:
        AdagentsValidationError: If adagents_data is malformed
    """
    properties = get_all_properties(adagents_data)
    tags = set()

    for prop in properties:
        prop_tags = prop.get("tags", [])
        if isinstance(prop_tags, list):
            for tag in prop_tags:
                if isinstance(tag, str):
                    tags.add(tag)

    return tags

Extract all unique tags from properties in adagents.json data.

Args
-----=
adagents_data
Parsed adagents.json data

Returns -----= Set of all unique tags across all properties

Raises
-----=
AdagentsValidationError
If adagents_data is malformed
def get_asset_count(format: FormatAssetsInput | Mapping[str, Any]) ‑> int
Expand source code
def get_asset_count(format: FormatAssetsInput | Mapping[str, Any]) -> int:
    """Get the count of assets in a format (for display purposes).

    Args:
        format: The Format object

    Returns:
        Number of assets, or 0 if none defined
    """
    return len(get_format_assets(format))

Get the count of assets in a format (for display purposes).

Args
-----=
format
The Format object

Returns -----= Number of assets, or 0 if none defined

def get_format_assets(format: FormatAssetsInput | Mapping[str, Any]) ‑> list[typing.Any]
Expand source code
def get_format_assets(format: FormatAssetsInput | Mapping[str, Any]) -> list[FormatAsset]:
    """Get assets from a Format.

    Returns the list of assets from the format's `assets` field.
    Returns empty list if no assets are defined (flexible format with no assets).

    Args:
        format: Any canonical declaration or raw catalog object exposing ``assets``

    Returns:
        List of assets

    Example:
        ```python
        for declaration in product.format_options:
            assets = get_format_assets(declaration)
            print(f"{declaration.format_kind} has {len(assets)} assets")
        ```
    """
    if isinstance(format, Mapping):
        assets = format.get("assets")
        if assets is None and isinstance(format.get("params"), Mapping):
            assets = format["params"].get("slots")
    else:
        assets = getattr(format, "assets", None)
        if assets is None:
            params = getattr(format, "params", None)
            assets = params.get("slots") if isinstance(params, Mapping) else None
    if assets:
        return list(assets)

    return []

Get assets from a Format.

Returns the list of assets from the format's assets field. Returns empty list if no assets are defined (flexible format with no assets).

Args
-----=
format
Any canonical declaration or raw catalog object exposing assets

Returns -----= List of assets

Example -----=

for declaration in product.format_options:
    assets = get_format_assets(declaration)
    print(f"{declaration.format_kind} has {len(assets)} assets")
def get_individual_assets(format: FormatAssetsInput | Mapping[str, Any]) ‑> list[typing.Any]
Expand source code
def get_individual_assets(format: FormatAssetsInput | Mapping[str, Any]) -> list[FormatAsset]:
    """Get individual assets (not repeatable groups) from a Format.

    Args:
        format: The Format object

    Returns:
        List of individual assets (item_type='individual')
    """
    return [asset for asset in get_format_assets(format) if _get_item_type(asset) == "individual"]

Get individual assets (not repeatable groups) from a Format.

Args
-----=
format
The Format object

Returns -----= List of individual assets (item_type='individual')

def get_optional_assets(format: FormatAssetsInput | Mapping[str, Any]) ‑> list[typing.Any]
Expand source code
def get_optional_assets(format: FormatAssetsInput | Mapping[str, Any]) -> list[FormatAsset]:
    """Get only optional assets from a Format.

    Note: When using deprecated `assets_required`, this will always return empty
    since assets_required only contained required assets.

    Args:
        format: The Format object

    Returns:
        List of optional assets only

    Example:
        ```python
        optional_assets = get_optional_assets(format)
        print(f"Can optionally provide {len(optional_assets)} additional assets")
        ```
    """
    return [asset for asset in get_format_assets(format) if not _is_required(asset)]

Get only optional assets from a Format.

Note: When using deprecated assets_required, this will always return empty since assets_required only contained required assets.

Args
-----=
format
The Format object

Returns -----= List of optional assets only

Example -----=

optional_assets = get_optional_assets(format)
print(f"Can optionally provide {len(optional_assets)} additional assets")
def get_properties_by_agent(adagents_data: dict[str, Any], agent_url: str) ‑> list[dict[str, typing.Any]]
Expand source code
def get_properties_by_agent(adagents_data: dict[str, Any], agent_url: str) -> list[dict[str, Any]]:
    """Get all properties authorized for a specific agent.

    Handles all authorization types per the AdCP specification:
    - inline_properties: Properties defined directly in the agent's properties array
    - property_ids: Filter top-level properties by property_id
    - property_tags: Filter top-level properties by tags
    - publisher_properties: Inline-resolved properties from cross-publisher
      selectors (resolved from the parent file's top-level properties[]
      array per adcp#4827)

    For ``publisher_properties`` selectors whose target ``publisher_domain``
    is NOT present inline in this file's top-level ``properties[]`` array,
    this function returns no properties for that selector. Federated
    fallback (fetching the child publisher's own adagents.json to resolve
    the selector remotely) is out of scope here and lives in
    :func:`fetch_agent_authorizations_from_directory` and
    :func:`detect_publisher_properties_divergence` from companion PR #752.
    Wire-only authorization checks that assume federated resolution will
    under-authorize against managed-network parent files that only inline
    a subset of their child domains.

    Args:
        adagents_data: Parsed adagents.json data
        agent_url: URL of the agent to filter by

    Returns:
        List of properties for the specified agent (empty if agent not found)

    Raises:
        AdagentsValidationError: If adagents_data is malformed
    """
    return _resolve_properties_for_agent(adagents_data, agent_url, permissive_bare_top_level=False)

Get all properties authorized for a specific agent.

Handles all authorization types per the AdCP specification: - inline_properties: Properties defined directly in the agent's properties array - property_ids: Filter top-level properties by property_id - property_tags: Filter top-level properties by tags - publisher_properties: Inline-resolved properties from cross-publisher selectors (resolved from the parent file's top-level properties[] array per adcp#4827)

For publisher_properties selectors whose target publisher_domain is NOT present inline in this file's top-level properties[] array, this function returns no properties for that selector. Federated fallback (fetching the child publisher's own adagents.json to resolve the selector remotely) is out of scope here and lives in :func:fetch_agent_authorizations_from_directory() and :func:detect_publisher_properties_divergence() from companion PR #752. Wire-only authorization checks that assume federated resolution will under-authorize against managed-network parent files that only inline a subset of their child domains.

Args
-----=
adagents_data
Parsed adagents.json data
agent_url
URL of the agent to filter by

Returns -----= List of properties for the specified agent (empty if agent not found)

Raises
-----=
AdagentsValidationError
If adagents_data is malformed
def get_repeatable_groups(format: FormatAssetsInput | Mapping[str, Any]) ‑> list[typing.Any]
Expand source code
def get_repeatable_groups(format: FormatAssetsInput | Mapping[str, Any]) -> list[FormatAsset]:
    """Get repeatable asset groups from a Format.

    Args:
        format: The Format object

    Returns:
        List of repeatable asset groups (item_type='repeatable_group')
    """
    return [
        asset for asset in get_format_assets(format) if _get_item_type(asset) == "repeatable_group"
    ]

Get repeatable asset groups from a Format.

Args
-----=
format
The Format object

Returns -----= List of repeatable asset groups (item_type='repeatable_group')

def get_required_assets(format: FormatAssetsInput | Mapping[str, Any]) ‑> list[typing.Any]
Expand source code
def get_required_assets(format: FormatAssetsInput | Mapping[str, Any]) -> list[FormatAsset]:
    """Get only required assets from a Format.

    Args:
        format: The Format object

    Returns:
        List of required assets only

    Example:
        ```python
        required_assets = get_required_assets(format)
        print(f"Must provide {len(required_assets)} assets")
        ```
    """
    return [asset for asset in get_format_assets(format) if _is_required(asset)]

Get only required assets from a Format.

Args
-----=
format
The Format object

Returns -----= List of required assets only

Example -----=

required_assets = get_required_assets(format)
print(f"Must provide {len(required_assets)} assets")
def has_assets(format: FormatAssetsInput | Mapping[str, Any]) ‑> bool
Expand source code
def has_assets(format: FormatAssetsInput | Mapping[str, Any]) -> bool:
    """Check if a format has any assets defined.

    Args:
        format: The Format object

    Returns:
        True if format has assets, False otherwise
    """
    return get_asset_count(format) > 0

Check if a format has any assets defined.

Args
-----=
format
The Format object

Returns -----= True if format has assets, False otherwise

def identifiers_match(property_identifiers: list[dict[str, str]],
agent_identifiers: list[dict[str, str]]) ‑> bool
Expand source code
def identifiers_match(
    property_identifiers: list[dict[str, str]],
    agent_identifiers: list[dict[str, str]],
) -> bool:
    """Check if any property identifier matches agent's authorized identifiers.

    Args:
        property_identifiers: Identifiers from property
            (e.g., [{"type": "domain", "value": "cnn.com"}])
        agent_identifiers: Identifiers from adagents.json

    Returns:
        True if any identifier matches

    Notes:
        - Domain identifiers use AdCP domain matching rules
        - Other identifiers (bundle_id, roku_store_id, etc.) require exact match
    """
    for prop_id in property_identifiers:
        prop_type = prop_id.get("type", "")
        prop_value = prop_id.get("value", "")

        for agent_id in agent_identifiers:
            agent_type = agent_id.get("type", "")
            agent_value = agent_id.get("value", "")

            # Type must match
            if prop_type != agent_type:
                continue

            # Domain identifiers use special matching rules
            if prop_type == "domain":
                if domain_matches(prop_value, agent_value):
                    return True
            else:
                # Other identifier types require exact match
                if prop_value == agent_value:
                    return True

    return False

Check if any property identifier matches agent's authorized identifiers.

Args
-----=
property_identifiers
Identifiers from property (e.g., [{"type": "domain", "value": "cnn.com"}])
agent_identifiers
Identifiers from adagents.json

Returns -----= True if any identifier matches

Notes -----= - Domain identifiers use AdCP domain matching rules - Other identifiers (bundle_id, roku_store_id, etc.) require exact match

def normalize_assets_required(assets_required: list[Any]) ‑> list[typing.Any]
Expand source code
def normalize_assets_required(assets_required: list[Any]) -> list[FormatAsset]:
    """Convert deprecated assets_required to new assets format.

    .. deprecated:: 3.2.0
        The ``assets_required`` field was removed in ADCP 3.0.0-beta.2.
        This function will be removed in a future version.

    All assets in assets_required are required by definition (that's why they were in
    that array). The new `assets` field has an explicit `required: boolean` to allow
    both required AND optional assets.

    Args:
        assets_required: The deprecated assets_required array

    Returns:
        Normalized assets as Pydantic models with explicit required=True
    """
    warnings.warn(
        "normalize_assets_required() is deprecated. "
        "The assets_required field was removed in ADCP 3.0.0-beta.2. "
        "This function will be removed in a future version.",
        DeprecationWarning,
        stacklevel=2,
    )
    normalized: list[FormatAsset] = []
    for asset in assets_required:
        # Get asset data as dict
        if isinstance(asset, dict):
            asset_dict = asset
        else:
            asset_dict = asset.model_dump() if hasattr(asset, "model_dump") else dict(asset)

        # Map old fields to new schema format
        mapped = {**asset_dict, "required": True}
        # Ensure asset_id is present (map from asset_group_id if needed)
        if "asset_group_id" in mapped and "asset_id" not in mapped:
            mapped["asset_id"] = mapped.pop("asset_group_id")
        # Remove fields that don't exist in the new schema
        for old_field in ("min_count", "max_count", "assets"):
            mapped.pop(old_field, None)
        # Use AssetsModel (individual asset type)
        normalized.append(AssetsModel(**mapped))

    return normalized

Convert deprecated assets_required to new assets format.

Deprecated since version: 3.2.0

The assets_required field was removed in ADCP 3.0.0-beta.2. This function will be removed in a future version.

All assets in assets_required are required by definition (that's why they were in that array). The new assets field has an explicit required: boolean to allow both required AND optional assets.

Args
-----=
assets_required
The deprecated assets_required array

Returns -----= Normalized assets as Pydantic models with explicit required=True

def resolve_properties_for_agent(adagents_data: dict[str, Any],
agent_url: str,
*,
mode: PropertyResolutionMode = 'strict') ‑> list[dict[str, typing.Any]]
Expand source code
def resolve_properties_for_agent(
    adagents_data: dict[str, Any],
    agent_url: str,
    *,
    mode: PropertyResolutionMode = "strict",
) -> list[dict[str, Any]]:
    """Resolve properties for an agent with an explicit strict/permissive mode.

    ``mode="strict"`` is identical to :func:`get_properties_by_agent` and
    only honors schema-conformant authorization selectors plus the historical
    inline ``properties`` legacy shape.

    ``mode="permissive"`` keeps every strict selector behavior unchanged, but
    treats one exact matching bare ``authorized_agents`` entry
    (``{"url": ..., "authorized_for": ...}``) as authorizing the file's
    top-level ``properties[]``. This is for operational binding of
    non-conformant publisher files that list an agent URL without an
    ``authorization_type`` or selector. If the agent is not listed, has any
    explicit or unknown selector field, or has multiple same-URL entries, the
    resolver still returns the strict result.

    Args:
        adagents_data: Parsed adagents.json data
        agent_url: URL of the agent to filter by
        mode: ``"strict"`` for spec-conformant resolution, ``"permissive"``
            to opt into bare-entry top-level property fallback.

    Returns:
        List of properties for the specified agent.

    Raises:
        AdagentsValidationError: If adagents_data is malformed
        ValueError: If mode is not ``"strict"`` or ``"permissive"``
    """
    if mode == "strict":
        return _resolve_properties_for_agent(
            adagents_data,
            agent_url,
            permissive_bare_top_level=False,
        )
    if mode == "permissive":
        return _resolve_properties_for_agent(
            adagents_data,
            agent_url,
            permissive_bare_top_level=True,
        )
    raise ValueError("mode must be 'strict' or 'permissive'")

Resolve properties for an agent with an explicit strict/permissive mode.

mode="strict" is identical to :func:get_properties_by_agent() and only honors schema-conformant authorization selectors plus the historical inline properties legacy shape.

mode="permissive" keeps every strict selector behavior unchanged, but treats one exact matching bare authorized_agents entry ({"url": ..., "authorized_for": ...}) as authorizing the file's top-level properties[]. This is for operational binding of non-conformant publisher files that list an agent URL without an authorization_type or selector. If the agent is not listed, has any explicit or unknown selector field, or has multiple same-URL entries, the resolver still returns the strict result.

Args
-----=
adagents_data
Parsed adagents.json data
agent_url
URL of the agent to filter by
mode
"strict" for spec-conformant resolution, "permissive" to opt into bare-entry top-level property fallback.

Returns -----= List of properties for the specified agent.

Raises
-----=
AdagentsValidationError
If adagents_data is malformed
ValueError
If mode is not "strict" or "permissive"
def sign_legacy_webhook(secret: str,
payload: dict[str, Any] | AdCPBaseModel,
*,
timestamp: str | int | None = None,
headers: dict[str, Any] | None = None) ‑> tuple[dict[str, str], bytes]
Expand source code
def sign_legacy_webhook(
    secret: str,
    payload: dict[str, Any] | AdCPBaseModel,
    *,
    timestamp: str | int | None = None,
    headers: dict[str, Any] | None = None,
) -> tuple[dict[str, str], bytes]:
    """Return ``(signed_headers, body_bytes)`` for a legacy HMAC webhook.

    Byte-equality between signature input and HTTP body is guaranteed —
    callers POST ``content=body_bytes`` instead of ``json=payload``, so the
    separator-drift trap that caused silent 401s in every spaced-vs-compact
    interop is structurally impossible here.

    This is a lower-level companion to :func:`deliver` for callers who need
    to own the HTTP transport themselves (custom auth, pre-configured
    ``httpx.AsyncClient``, non-httpx clients). For the one-shot "send a
    webhook" path, prefer :func:`deliver`.

    The returned ``body_bytes`` use compact separators (``","``/``":"``)
    matching the canonical on-wire form pinned by adcontextprotocol/adcp#2478.

    Example:
        >>> signed, body = sign_legacy_webhook("shared-secret", payload)
        >>> headers = {**signed, "Content-Type": "application/json"}
        >>> await client.post(url, content=body, headers=headers)
    """
    signature_headers, body_bytes = _compute_legacy_signature(
        secret=secret, timestamp=timestamp, payload=payload
    )
    if headers is not None:
        merged = {str(k): str(v) for k, v in headers.items()}
        merged.update(signature_headers)
        return merged, body_bytes
    return signature_headers, body_bytes

Return (signed_headers, body_bytes) for a legacy HMAC webhook.

Byte-equality between signature input and HTTP body is guaranteed — callers POST content=body_bytes instead of json=payload, so the separator-drift trap that caused silent 401s in every spaced-vs-compact interop is structurally impossible here.

This is a lower-level companion to :func:deliver for callers who need to own the HTTP transport themselves (custom auth, pre-configured httpx.AsyncClient, non-httpx clients). For the one-shot "send a webhook" path, prefer :func:deliver.

The returned body_bytes use compact separators (","/":") matching the canonical on-wire form pinned by adcontextprotocol/adcp#2478.

Example -----=

>>> signed, body = sign_legacy_webhook("shared-secret", payload)
>>> headers = {**signed, "Content-Type": "application/json"}
>>> await client.post(url, content=body, headers=headers)
def sign_webhook(*,
method: str,
url: str,
headers: Mapping[str, str],
body: bytes,
private_key: PrivateKey,
key_id: str,
alg: str,
created: int | None = None,
expires_in_seconds: int = 300,
nonce: str | None = None,
label: str = 'sig1') ‑> SignedHeaders
Expand source code
def sign_webhook(
    *,
    method: str,
    url: str,
    headers: Mapping[str, str],
    body: bytes,
    private_key: PrivateKey,
    key_id: str,
    alg: str,
    created: int | None = None,
    expires_in_seconds: int = DEFAULT_EXPIRES_IN_SECONDS,
    nonce: str | None = None,
    label: str = SIG_LABEL_DEFAULT,
) -> SignedHeaders:
    """Sign an outgoing webhook POST per adcp/webhook-signing/v1.

    ``cover_content_digest=True`` and ``tag=WEBHOOK_TAG`` are pinned. The
    caller attaches ``SignedHeaders.as_dict()`` to the outgoing HTTP request.

    The ``method`` is normally ``"POST"`` for webhook delivery; passed through
    unchanged so callers signing a retried ``PUT`` or variant delivery verb
    are not forced into an extra translation.

    See also:
        :class:`adcp.webhooks.WebhookSender` — higher-level one-call helper
        that builds the payload, signs, and POSTs in a single call. Prefer it
        unless you need to own the HTTP transport yourself.
    """
    return sign_request(
        method=method,
        url=url,
        headers=headers,
        body=body,
        private_key=private_key,
        key_id=key_id,
        alg=alg,
        cover_content_digest=True,
        created=created,
        expires_in_seconds=expires_in_seconds,
        nonce=nonce,
        tag=WEBHOOK_TAG,
        label=label,
        signing_profile_version="3.2",
    )

Sign an outgoing webhook POST per adcp/webhook-signing/v1.

cover_content_digest=True and tag=WEBHOOK_TAG are pinned. The caller attaches SignedHeaders.as_dict() to the outgoing HTTP request.

The method is normally "POST" for webhook delivery; passed through unchanged so callers signing a retried PUT or variant delivery verb are not forced into an extra translation.

See also: :class:WebhookSender — higher-level one-call helper that builds the payload, signs, and POSTs in a single call. Prefer it unless you need to own the HTTP transport yourself.

def to_wire_dict(payload: AdCPBaseModel | Task | TaskStatusUpdateEvent | Mapping[str, Any]) ‑> dict[str, typing.Any]
Expand source code
def to_wire_dict(
    payload: AdCPBaseModel | Task | TaskStatusUpdateEvent | Mapping[str, Any],
) -> dict[str, Any]:
    """Serialize any AdCP webhook payload to a JSON-ready dict.

    Single seam for adopters that accept "any AdCP webhook payload" — a
    sender wrapping :func:`create_a2a_webhook_payload` and
    :func:`create_mcp_webhook_payload` would otherwise have to write
    per-shape dispatch (``isinstance`` checks, ``MessageToDict`` for
    protobuf, ``model_dump`` for Pydantic, passthrough for dict). Brittle:
    a future a2a-sdk that swaps protobuf for a Pydantic façade silently
    changes which branch runs, and adopters duplicate the dispatch in
    every send path. Use this helper instead — the dispatch lives here.

    Behaviour by input shape:

    * a2a ``Task`` / ``TaskStatusUpdateEvent`` (protobuf, a2a-sdk 1.0+) →
      ``MessageToDict(..., preserving_proto_field_name=False)`` so JSON
      keys match the A2A wire spec (camelCase: ``id``, ``contextId``,
      ``artifactId``). Enum values are normalized from the 1.0 protobuf
      form (``TASK_STATE_COMPLETED``, ``ROLE_AGENT``) to the 0.3-spec
      lowercase form (``completed``, ``agent``) so 0.3 buyer receivers
      keep parsing.
    * Any Pydantic model (``McpWebhookPayload``, future Pydantic façades,
      :class:`AdCPBaseModel` subclasses) → ``model_dump(mode="json",
      exclude_none=True)``.
    * ``Mapping`` → coerced to ``dict``. Legacy adopter passthrough for
      callers that build the wire dict by hand.

    Raises:
        TypeError: payload is none of the above.
    """
    if isinstance(payload, (Task, TaskStatusUpdateEvent)):
        data = MessageToDict(payload, preserving_proto_field_name=False)
        _normalize_a2a_task_state_to_v03(data)
        return data
    if hasattr(payload, "model_dump"):
        model = cast(AdCPBaseModel, payload)
        return model.model_dump(mode="json", exclude_none=True)
    if isinstance(payload, Mapping):
        return dict(payload)
    raise TypeError(
        f"Unsupported webhook payload type {type(payload).__name__}: expected "
        "a2a Task / TaskStatusUpdateEvent (protobuf), an AdCP Pydantic model "
        "(e.g. McpWebhookPayload), or a Mapping[str, Any]."
    )

Serialize any AdCP webhook payload to a JSON-ready dict.

Single seam for adopters that accept "any AdCP webhook payload" — a sender wrapping :func:create_a2a_webhook_payload() and :func:create_mcp_webhook_payload() would otherwise have to write per-shape dispatch (isinstance checks, MessageToDict for protobuf, model_dump for Pydantic, passthrough for dict). Brittle: a future a2a-sdk that swaps protobuf for a Pydantic façade silently changes which branch runs, and adopters duplicate the dispatch in every send path. Use this helper instead — the dispatch lives here.

Behaviour by input shape:

  • a2a Task / TaskStatusUpdateEvent (protobuf, a2a-sdk 1.0+) → MessageToDict(..., preserving_proto_field_name=False) so JSON keys match the A2A wire spec (camelCase: id, contextId, artifactId). Enum values are normalized from the 1.0 protobuf form (TASK_STATE_COMPLETED, ROLE_AGENT) to the 0.3-spec lowercase form (completed, agent) so 0.3 buyer receivers keep parsing.
  • Any Pydantic model (McpWebhookPayload, future Pydantic façades, :class:AdCPBaseModel subclasses) → model_dump(mode="json", exclude_none=True).
  • Mapping → coerced to dict. Legacy adopter passthrough for callers that build the wire dict by hand.
Raises
-----=
TypeError
payload is none of the above.
def translate_universal_macros(pixel_url: str, mapping: MacroMapping) ‑> TranslateUniversalMacrosResult
Expand source code
def translate_universal_macros(
    pixel_url: str,
    mapping: MacroMapping,
) -> TranslateUniversalMacrosResult:
    """Translate universal macros in query-parameter values.

    ``ValueMacroMapping`` values are UTF-8 percent-encoded with
    :func:`encode_unreserved`; ``NativeMacroMapping`` values are inserted
    verbatim after the full mapping passes the control-character guard. If a
    parameter value contains any unmapped universal macro, that whole parameter
    is dropped. Query keys, the path, the fragment, and parameters without
    universal macros pass through byte-for-byte. Replacement is single-pass.

    Consent macros supplied through ``ValueMacroMapping`` are translated but
    reported in ``frozen_consent_macros`` because freezing impression-time
    consent at producer time can create a privacy defect. Callers should also
    inspect ``dropped_consent_macros`` and ``suspect_native_values`` before
    publishing a tracker.

    Raises:
        UniversalMacroTranslationError: A native mapping, used or unused,
            contains U+0000-U+001F or U+007F. No URL is emitted.
        TypeError: A mapping entry is not a supported typed mapping model.
    """

    frozen_consent_macros: list[str] = []
    frozen_seen: set[str] = set()
    suspect_native_values: list[str] = []
    suspect_seen: set[str] = set()
    validated_mapping: dict[str, MacroMappingEntry] = {}

    # Validate the entire raw-token trust boundary before doing any URL work.
    # An unused unsafe entry must reject just like an entry present in the URL.
    for macro, entry in mapping.items():
        if isinstance(entry, NativeMacroMapping):
            native = entry.native
            if _has_unsafe_native_character(native):
                raise UniversalMacroTranslationError(macro)
            validated_mapping[macro] = NativeMacroMapping(native=native)
        elif isinstance(entry, ValueMacroMapping):
            value = entry.value
            if macro in _CONSENT_MACROS:
                _append_once(frozen_consent_macros, frozen_seen, macro)
            if _NATIVE_TOKEN_SHAPE.fullmatch(value):
                _append_once(suspect_native_values, suspect_seen, macro)
            validated_mapping[macro] = ValueMacroMapping(value=value)
        else:
            raise TypeError(
                f"mapping entry for {macro!r} must be NativeMacroMapping " "or ValueMacroMapping"
            )

    fragment_index = pixel_url.find("#")
    if fragment_index == -1:
        without_fragment = pixel_url
        fragment = ""
    else:
        without_fragment = pixel_url[:fragment_index]
        fragment = pixel_url[fragment_index:]

    query_index = without_fragment.find("?")
    if query_index == -1:
        return TranslateUniversalMacrosResult(
            url=pixel_url,
            dropped_params=[],
            unmapped_macros=[],
            dropped_consent_macros=[],
            frozen_consent_macros=frozen_consent_macros,
            suspect_native_values=suspect_native_values,
        )

    base = without_fragment[:query_index]
    raw_query = without_fragment[query_index + 1 :]

    dropped_params: list[str] = []
    unmapped_macros: list[str] = []
    unmapped_seen: set[str] = set()
    dropped_consent_macros: list[str] = []
    dropped_consent_seen: set[str] = set()
    output_parts: list[str] = []

    for raw_param in raw_query.split("&"):
        key, separator, value = raw_param.partition("=")
        tokens = _UNIVERSAL_MACRO.findall(value)
        if not tokens:
            output_parts.append(raw_param)
            continue

        missing = [token for token in tokens if token not in validated_mapping]
        if missing:
            dropped_params.append(key)
            for macro in missing:
                _append_once(unmapped_macros, unmapped_seen, macro)
                if macro in _CONSENT_MACROS:
                    _append_once(
                        dropped_consent_macros,
                        dropped_consent_seen,
                        macro,
                    )
            continue

        def replace(match: re.Match[str]) -> str:
            macro = match.group(0)
            entry = validated_mapping[macro]
            if isinstance(entry, NativeMacroMapping):
                return entry.native
            if isinstance(entry, ValueMacroMapping):
                return encode_unreserved(entry.value)
            # The mapping-wide validation above makes this unreachable even
            # for mutable custom Mapping implementations under normal use.
            raise TypeError(f"unsupported mapping entry for {macro!r}")

        translated = _UNIVERSAL_MACRO.sub(replace, value)
        output_parts.append(f"{key}{separator}{translated}")

    new_query = "&".join(output_parts)
    url = f"{base}?{new_query}{fragment}" if new_query else f"{base}{fragment}"
    return TranslateUniversalMacrosResult(
        url=url,
        dropped_params=dropped_params,
        unmapped_macros=unmapped_macros,
        dropped_consent_macros=dropped_consent_macros,
        frozen_consent_macros=frozen_consent_macros,
        suspect_native_values=suspect_native_values,
    )

Translate universal macros in query-parameter values.

ValueMacroMapping values are UTF-8 percent-encoded with :func:encode_unreserved(); NativeMacroMapping values are inserted verbatim after the full mapping passes the control-character guard. If a parameter value contains any unmapped universal macro, that whole parameter is dropped. Query keys, the path, the fragment, and parameters without universal macros pass through byte-for-byte. Replacement is single-pass.

Consent macros supplied through ValueMacroMapping are translated but reported in frozen_consent_macros because freezing impression-time consent at producer time can create a privacy defect. Callers should also inspect dropped_consent_macros and suspect_native_values before publishing a tracker.

Raises
-----=
UniversalMacroTranslationError
A native mapping, used or unused, contains U+0000-U+001F or U+007F. No URL is emitted.
TypeError
A mapping entry is not a supported typed mapping model.
def upgrade_legacy_format_id(value: str | FormatId | Mapping[str, Any],
*,
default_agent_url: str = 'https://creative.adcontextprotocol.org') ‑> LegacyFormatId
Expand source code
def upgrade_legacy_format_id(
    value: str | FormatId | Mapping[str, Any],
    *,
    default_agent_url: str = CANONICAL_CREATIVE_AGENT_URL,
) -> FormatId:
    """Return ``value`` as a canonical, parameterized ``FormatId`` when known.

    The current canonical upgrade maps legacy display size IDs such as
    ``display_300x250`` and ``display_300x250_image`` to
    ``display_image`` with ``width=300`` and ``height=250``. Unknown IDs are
    still returned as structured ``FormatId`` values so callers can compare
    them consistently.
    """
    is_bare_legacy_id = isinstance(value, str)
    fid = _coerce_format_id(value, default_agent_url=default_agent_url)
    match = _DISPLAY_SIZE_RE.fullmatch(fid.id)
    if match is None:
        return fid
    default_fid = _coerce_format_id("__default__", default_agent_url=default_agent_url)
    if not is_bare_legacy_id and canonicalize_agent_url(fid.agent_url) != canonicalize_agent_url(
        default_fid.agent_url
    ):
        return fid

    return FormatId.model_validate(
        {
            "agent_url": str(fid.agent_url),
            "id": "display_image",
            "width": int(match.group("width")),
            "height": int(match.group("height")),
            "duration_ms": fid.duration_ms,
        }
    )

Return value as a canonical, parameterized FormatId when known.

The current canonical upgrade maps legacy display size IDs such as display_300x250 and display_300x250_image to display_image with width=300 and height=250. Unknown IDs are still returned as structured FormatId values so callers can compare them consistently.

def uses_deprecated_assets_field(format: FormatAssetsInput | Mapping[str, Any]) ‑> bool
Expand source code
def uses_deprecated_assets_field(format: FormatAssetsInput | Mapping[str, Any]) -> bool:
    """Check if format uses deprecated assets_required field.

    .. deprecated:: 3.2.0
        The ``assets_required`` field was removed in ADCP 3.0.0-beta.2.
        This function always returns False and will be removed in a future version.

    Args:
        format: The Format object

    Returns:
        Always False (deprecated field no longer exists)
    """
    warnings.warn(
        "uses_deprecated_assets_field() is deprecated and always returns False. "
        "The assets_required field was removed in ADCP 3.0.0-beta.2. "
        "This function will be removed in a future version.",
        DeprecationWarning,
        stacklevel=2,
    )
    return False

Check if format uses deprecated assets_required field.

Deprecated since version: 3.2.0

The assets_required field was removed in ADCP 3.0.0-beta.2. This function always returns False and will be removed in a future version.

Args
-----=
format
The Format object

Returns -----= Always False (deprecated field no longer exists)

def validate_adagents(adagents: dict[str, Any]) ‑> None
Expand source code
def validate_adagents(adagents: dict[str, Any]) -> None:
    """Validate an adagents.json structure.

    Args:
        adagents: The adagents.json dict

    Raises:
        ValidationError: If validation fails
    """
    authorized_agents = adagents.get("authorized_agents")
    if isinstance(authorized_agents, list):
        for agent in authorized_agents:
            if isinstance(agent, dict):
                validate_agent_authorization(agent)

    revoked = adagents.get("revoked_publisher_domains")
    if revoked is not None:
        if not isinstance(revoked, list):
            raise ValidationError("'revoked_publisher_domains' must be an array")
        for entry in revoked:
            if not isinstance(entry, dict):
                raise ValidationError("revoked_publisher_domains entry must be an object")
            validate_revoked_publisher_domain_entry(entry)

Validate an adagents.json structure.

Args
-----=
adagents
The adagents.json dict
Raises
-----=
ValidationError
If validation fails
async def validate_adagents_domain(publisher_domain: str,
timeout: float = 10.0,
user_agent: str = 'AdCP-Client/1.0',
client: httpx.AsyncClient | None = None) ‑> AdAgentsValidationResult
Expand source code
async def validate_adagents_domain(
    publisher_domain: str,
    timeout: float = 10.0,
    user_agent: str = "AdCP-Client/1.0",
    client: httpx.AsyncClient | None = None,
) -> AdAgentsValidationResult:
    """Discover and validate a publisher's adagents.json with provenance.

    Mirrors :func:`fetch_adagents` discovery semantics but returns a
    typed :class:`AdAgentsValidationResult` exposing which path
    produced the data (``discovery_method``) and the manager domain
    used for the RFC 4175 fallback (``manager_domain``), if any.

    Errors are reported on the result rather than raised. A manager
    domain 404 is a terminal failure: ``valid`` is False and
    ``manager_domain`` is recorded for diagnostics.

    .. warning::

        When ``discovery_method == 'ads_txt_managerdomain'`` the data
        came from the manager, not the publisher. Callers wiring this
        into authorization decisions must verify that the source
        publisher is explicitly named in the manager's adagents.json
        (e.g., via ``publisher_properties.publisher_domain`` on the
        relevant authorized_agents entry) before trusting an agent
        claim — otherwise a manager that lists agent A unconditionally
        implicitly authorizes A for every publisher pointing
        MANAGERDOMAIN at the manager.
    """
    try:
        normalized = _validate_publisher_domain(publisher_domain)
    except AdagentsValidationError as e:
        return AdAgentsValidationResult(
            domain=publisher_domain,
            url="",
            errors=[str(e)],
        )

    url = f"https://{normalized}/.well-known/adagents.json"

    try:
        data, discovery, *_ = await _resolve_direct(normalized, timeout, user_agent, client)
        return AdAgentsValidationResult(
            domain=normalized,
            url=url,
            discovery_method=discovery,
            data=data,
            valid=True,
        )
    except AdagentsNotFoundError as direct_error:
        direct_error_msg = str(direct_error)
    except (AdagentsValidationError, AdagentsTimeoutError) as e:
        return AdAgentsValidationResult(
            domain=normalized,
            url=url,
            errors=[str(e)],
        )

    managers = await _fetch_ads_txt_managerdomains(normalized, timeout, user_agent, client)
    if not managers:
        return AdAgentsValidationResult(
            domain=normalized,
            url=url,
            errors=[direct_error_msg],
        )

    manager_domain = managers[-1]

    if manager_domain == normalized:
        return AdAgentsValidationResult(
            domain=normalized,
            url=url,
            errors=[
                direct_error_msg,
                f"ads.txt managerdomain {manager_domain} points back to source publisher",
            ],
        )

    manager_normalized = _ensure_safe_manager_domain(manager_domain)
    if manager_normalized is None:
        return AdAgentsValidationResult(
            domain=normalized,
            url=url,
            errors=[
                direct_error_msg,
                f"ads.txt managerdomain {manager_domain!r} is malformed or "
                "targets a private/reserved address",
            ],
        )

    try:
        manager_data, *_ = await _resolve_direct(
            manager_normalized, timeout, user_agent, client=None
        )
    except AdagentsNotFoundError:
        return AdAgentsValidationResult(
            domain=normalized,
            url=url,
            discovery_method="ads_txt_managerdomain",
            manager_domain=manager_normalized,
            errors=[
                direct_error_msg,
                f"manager domain {manager_normalized} did not serve adagents.json",
            ],
        )
    except (AdagentsValidationError, AdagentsTimeoutError) as e:
        return AdAgentsValidationResult(
            domain=normalized,
            url=url,
            discovery_method="ads_txt_managerdomain",
            manager_domain=manager_normalized,
            errors=[direct_error_msg, str(e)],
        )

    return AdAgentsValidationResult(
        domain=normalized,
        url=url,
        discovery_method="ads_txt_managerdomain",
        manager_domain=manager_normalized,
        data=manager_data,
        valid=True,
    )

Discover and validate a publisher's adagents.json with provenance.

Mirrors :func:fetch_adagents() discovery semantics but returns a typed :class:AdAgentsValidationResult exposing which path produced the data (discovery_method) and the manager domain used for the RFC 4175 fallback (manager_domain), if any.

Errors are reported on the result rather than raised. A manager domain 404 is a terminal failure: valid is False and manager_domain is recorded for diagnostics.

Warning

When discovery_method == 'ads_txt_managerdomain' the data came from the manager, not the publisher. Callers wiring this into authorization decisions must verify that the source publisher is explicitly named in the manager's adagents.json (e.g., via publisher_properties.publisher_domain on the relevant authorized_agents entry) before trusting an agent claim — otherwise a manager that lists agent A unconditionally implicitly authorizes A for every publisher pointing MANAGERDOMAIN at the manager.

def validate_adagents_structure(adagents_data: dict[str, Any], *, source_url: str | None = None) ‑> AdagentsValidationReport
Expand source code
def validate_adagents_structure(
    adagents_data: dict[str, Any], *, source_url: str | None = None
) -> AdagentsValidationReport:
    """Structurally validate a parsed adagents.json against the AdCP schema.

    Use this to distinguish a schema-invalid file from a valid file that
    doesn't list a particular agent. :func:`get_properties_by_agent`
    returns ``[]`` for both cases, which makes "publisher hasn't
    authorized us yet" indistinguishable from "publisher's file is
    structurally broken." This helper reports per-entry violations
    against the authoritative ``authorized_agents`` oneOf in the AdCP
    adagents.json schema.

    The two real-world failure modes this catches in production
    publisher files are:

    * **Bare entries** — ``{url, authorized_for}`` with no
      ``authorization_type``. The agent looks listed, but matches no
      schema variant, so the SDK treats the entry as authorizing
      nothing.
    * **Wrong selector for type** — e.g.,
      ``{authorization_type: "property_ids", property_tags: [...]}``,
      where the discriminator and selector array disagree.

    Args:
        adagents_data: Parsed adagents.json (the dict returned by
            :func:`fetch_adagents` or loaded directly from JSON).
        source_url: Final URL that supplied the document. Required when
            validating a catalog containing ``reference_renderer`` because
            ``catalog_role`` is self-declared and does not establish trust.

    Returns:
        :class:`AdagentsValidationReport`. ``schema_valid`` is True only
        when every entry in ``authorized_agents`` satisfies the schema.

    Raises:
        AdagentsValidationError: If ``adagents_data`` is not a dict, or
            ``authorized_agents`` is not a list. These are
            input-shape errors, not per-entry schema violations.

    Notes:
        * URL-reference variants (``authoritative_location`` form) have
          no inline ``authorized_agents`` array. They're reported with
          ``is_reference=True``, ``authorized_agents_count == 0``, and
          ``schema_valid=True``. Callers should follow the redirect
          (e.g., via :func:`fetch_adagents`, which resolves it
          automatically) and re-validate the resolved file.
        * The schema targets AdCP 3.2. Files written against 2.5 (no
          signal_ids / signal_tags variants) will flag those entries as
          ``unknown_authorization_type`` — correct for the 3.2 target,
          but worth knowing if you're validating mixed-version traffic.
        * Selector-array *item* patterns (e.g., the
          ``^[a-zA-Z0-9_-]+$`` constraint on each signal_id) are out of
          scope. This helper validates the discriminator + required
          selector array; it does not deep-validate selector contents.
    """
    if not isinstance(adagents_data, dict):
        raise AdagentsValidationError("adagents_data must be a dictionary")

    authorized_agents = adagents_data.get("authorized_agents")
    if authorized_agents is None:
        # URL-reference variant: file points at an authoritative_location
        # rather than carrying an inline authorized_agents array.
        properties = adagents_data.get("properties", [])
        is_reference = isinstance(adagents_data.get("authoritative_location"), str)
        missing_errors: list[AdagentsEntryError] = []
        if not is_reference:
            missing_errors.append(
                AdagentsEntryError(
                    index=-1,
                    kind="missing_authorized_agents",
                    message="inline adagents.json requires an authorized_agents array",
                )
            )
        return AdagentsValidationReport(
            schema_valid=not missing_errors,
            errors=missing_errors,
            authorized_agents_count=0,
            properties_count=len(properties) if isinstance(properties, list) else 0,
            is_reference=is_reference,
        )

    if not isinstance(authorized_agents, list):
        raise AdagentsValidationError("'authorized_agents' must be an array")

    properties = adagents_data.get("properties", [])
    properties_count = len(properties) if isinstance(properties, list) else 0

    errors: list[AdagentsEntryError] = []

    has_catalog_content = any(
        isinstance(adagents_data.get(field_name), list) and adagents_data[field_name]
        for field_name in _CATALOG_CONTENT_FIELDS
    )
    if len(authorized_agents) == 0 and not has_catalog_content:
        # Beta.3 permits an empty authorization list for catalog-only files,
        # but a file with neither authorization nor catalog content is invalid.
        errors.append(
            AdagentsEntryError(
                index=-1,
                kind="empty_authorized_agents",
                message=(
                    "adagents.json requires at least one authorized agent or a non-empty "
                    "formats, properties, placements, collections, or signals catalog"
                ),
            )
        )

    errors.extend(_reference_renderer_catalog_errors(adagents_data, source_url=source_url))

    for index, entry in enumerate(authorized_agents):
        if not isinstance(entry, dict):
            errors.append(
                AdagentsEntryError(
                    index=index,
                    kind="not_an_object",
                    message=f"authorized_agents[{index}] is not a JSON object",
                )
            )
            continue

        raw_url = entry.get("url")
        url = raw_url if isinstance(raw_url, str) and raw_url else None

        if url is None:
            errors.append(
                AdagentsEntryError(
                    index=index,
                    kind="missing_url",
                    message=f"authorized_agents[{index}] is missing required 'url'",
                )
            )

        authorized_for = entry.get("authorized_for")
        if not isinstance(authorized_for, str) or not authorized_for:
            errors.append(
                AdagentsEntryError(
                    index=index,
                    kind="missing_authorized_for",
                    message=(
                        f"authorized_agents[{index}] is missing required "
                        "'authorized_for' description (string, minLength 1)"
                    ),
                    url=url,
                )
            )

        authorization_type = entry.get("authorization_type")
        if authorization_type is None:
            errors.append(
                AdagentsEntryError(
                    index=index,
                    kind="missing_authorization_type",
                    message=(
                        f"authorized_agents[{index}] is missing required "
                        "'authorization_type' discriminator (expected one of: "
                        f"{', '.join(sorted(_AUTHORIZATION_TYPE_TO_SELECTOR))})"
                    ),
                    url=url,
                )
            )
            continue

        if authorization_type not in _AUTHORIZATION_TYPE_TO_SELECTOR:
            errors.append(
                AdagentsEntryError(
                    index=index,
                    kind="unknown_authorization_type",
                    message=(
                        f"authorized_agents[{index}] has unknown "
                        f"authorization_type={authorization_type!r} "
                        f"(expected one of: "
                        f"{', '.join(sorted(_AUTHORIZATION_TYPE_TO_SELECTOR))})"
                    ),
                    url=url,
                )
            )
            continue

        required_selector = _AUTHORIZATION_TYPE_TO_SELECTOR[authorization_type]
        selector_value = entry.get(required_selector)
        if not isinstance(selector_value, list) or len(selector_value) == 0:
            errors.append(
                AdagentsEntryError(
                    index=index,
                    kind="missing_selector_for_type",
                    message=(
                        f"authorized_agents[{index}] has "
                        f"authorization_type={authorization_type!r} but is "
                        f"missing required non-empty {required_selector!r} array"
                    ),
                    url=url,
                )
            )

    return AdagentsValidationReport(
        schema_valid=not errors,
        errors=errors,
        authorized_agents_count=len(authorized_agents),
        properties_count=properties_count,
    )

Structurally validate a parsed adagents.json against the AdCP schema.

Use this to distinguish a schema-invalid file from a valid file that doesn't list a particular agent. :func:get_properties_by_agent() returns [] for both cases, which makes "publisher hasn't authorized us yet" indistinguishable from "publisher's file is structurally broken." This helper reports per-entry violations against the authoritative authorized_agents oneOf in the AdCP adagents.json schema.

The two real-world failure modes this catches in production publisher files are:

  • Bare entries{url, authorized_for} with no authorization_type. The agent looks listed, but matches no schema variant, so the SDK treats the entry as authorizing nothing.
  • Wrong selector for type — e.g., {authorization_type: "property_ids", property_tags: [...]}, where the discriminator and selector array disagree.
Args
-----=
adagents_data
Parsed adagents.json (the dict returned by :func:fetch_adagents() or loaded directly from JSON).
source_url
Final URL that supplied the document. Required when validating a catalog containing reference_renderer because catalog_role is self-declared and does not establish trust.

Returns -----= :class:AdagentsValidationReport. schema_valid is True only when every entry in authorized_agents satisfies the schema.

Raises
-----=
AdagentsValidationError
If adagents_data is not a dict, or authorized_agents is not a list. These are input-shape errors, not per-entry schema violations.

Notes -----= * URL-reference variants (authoritative_location form) have no inline authorized_agents array. They're reported with is_reference=True, authorized_agents_count == 0, and schema_valid=True. Callers should follow the redirect (e.g., via :func:fetch_adagents(), which resolves it automatically) and re-validate the resolved file. * The schema targets AdCP 3.2. Files written against 2.5 (no signal_ids / signal_tags variants) will flag those entries as unknown_authorization_type — correct for the 3.2 target, but worth knowing if you're validating mixed-version traffic. * Selector-array item patterns (e.g., the ^[a-zA-Z0-9_-]+$ constraint on each signal_id) are out of scope. This helper validates the discriminator + required selector array; it does not deep-validate selector contents.

def validate_agent_authorization(agent: dict[str, Any]) ‑> None
Expand source code
def validate_agent_authorization(agent: dict[str, Any]) -> None:
    """Validate agent authorization discriminated union.

    AdCP v2.4.0+ uses discriminated unions with authorization_type discriminator:
    - authorization_type: "property_ids" requires property_ids
    - authorization_type: "property_tags" requires property_tags
    - authorization_type: "inline_properties" requires properties
    - authorization_type: "publisher_properties" requires publisher_properties

    For backward compatibility, also validates the old mutual exclusivity constraint.

    Args:
        agent: An agent dict from adagents.json

    Raises:
        ValidationError: If discriminator or field constraints are violated
    """
    authorization_type = agent.get("authorization_type")
    auth_fields = ["properties", "property_ids", "property_tags", "publisher_properties"]
    present_fields = [field for field in auth_fields if field in agent and agent[field] is not None]

    # If authorization_type discriminator is present, validate discriminated union
    if authorization_type:
        if authorization_type == "property_ids" and "property_ids" not in present_fields:
            raise ValidationError(
                "Agent with authorization_type='property_ids' must have property_ids"
            )
        elif authorization_type == "property_tags" and "property_tags" not in present_fields:
            raise ValidationError(
                "Agent with authorization_type='property_tags' must have property_tags"
            )
        elif authorization_type == "inline_properties" and "properties" not in present_fields:
            raise ValidationError(
                "Agent with authorization_type='inline_properties' must have properties"
            )
        elif (
            authorization_type == "publisher_properties"
            and "publisher_properties" not in present_fields
        ):
            raise ValidationError(
                "Agent with authorization_type='publisher_properties' "
                "must have publisher_properties"
            )
        elif authorization_type not in (
            "property_ids",
            "property_tags",
            "inline_properties",
            "publisher_properties",
        ):
            raise ValidationError(f"Agent has invalid authorization_type: {authorization_type}")

    # Validate mutual exclusivity (for both old and new formats)
    if len(present_fields) > 1:
        raise ValidationError(
            f"Agent authorization cannot have multiple fields: {', '.join(present_fields)}. "
            f"Only one of {', '.join(auth_fields)} is allowed."
        )

    if len(present_fields) == 0:
        raise ValidationError(
            f"Agent authorization must have exactly one of: {', '.join(auth_fields)}."
        )

    # If using publisher_properties, validate each item
    if "publisher_properties" in present_fields:
        for pub_prop in agent["publisher_properties"]:
            validate_publisher_properties_item(pub_prop)

Validate agent authorization discriminated union.

AdCP v2.4.0+ uses discriminated unions with authorization_type discriminator: - authorization_type: "property_ids" requires property_ids - authorization_type: "property_tags" requires property_tags - authorization_type: "inline_properties" requires properties - authorization_type: "publisher_properties" requires publisher_properties

For backward compatibility, also validates the old mutual exclusivity constraint.

Args
-----=
agent
An agent dict from adagents.json
Raises
-----=
ValidationError
If discriminator or field constraints are violated
def validate_capabilities(handler: Any, capabilities: GetAdcpCapabilitiesResponse) ‑> list[str]
Expand source code
def validate_capabilities(
    handler: Any,
    capabilities: GetAdcpCapabilitiesResponse,
) -> list[str]:
    """Check that a handler implements the methods required by its declared features.

    Compares the features declared in a capabilities response against the handler's
    method implementations. Returns warnings for features that are declared but
    whose corresponding handler methods are not overridden from the base class.

    This is a development-time check — call it at startup to catch misconfigurations.

    Args:
        handler: An ADCPHandler instance (or any object with handler methods).
        capabilities: The capabilities response the handler will serve.

    Returns:
        List of warning strings. Empty if everything is consistent.
    """
    # Late import to avoid circular dependency: server.base imports from adcp.types
    # which may transitively import from this module.
    from adcp.server.base import ADCPHandler

    resolver = FeatureResolver(capabilities)
    warnings: list[str] = []

    for feature, handler_methods in FEATURE_HANDLER_MAP.items():
        if not resolver.supports(feature):
            continue

        for method_name in handler_methods:
            if not hasattr(handler, method_name):
                warnings.append(
                    f"Feature '{feature}' is declared but handler has no " f"'{method_name}' method"
                )
                continue

            # Walk MRO to check if any class between the leaf and ADCPHandler
            # overrides the method (handles mixin / intermediate-class patterns).
            if isinstance(handler, ADCPHandler):
                overridden = any(
                    method_name in cls.__dict__
                    for cls in type(handler).__mro__
                    if cls is not ADCPHandler and not issubclass(ADCPHandler, cls)
                )
                if not overridden:
                    warnings.append(
                        f"Feature '{feature}' is declared but '{method_name}' "
                        f"is not overridden from ADCPHandler"
                    )

    return warnings

Check that a handler implements the methods required by its declared features.

Compares the features declared in a capabilities response against the handler's method implementations. Returns warnings for features that are declared but whose corresponding handler methods are not overridden from the base class.

This is a development-time check — call it at startup to catch misconfigurations.

Args
-----=
handler
An ADCPHandler instance (or any object with handler methods).
capabilities
The capabilities response the handler will serve.

Returns -----= List of warning strings. Empty if everything is consistent.

def validate_product(product: dict[str, Any]) ‑> None
Expand source code
def validate_product(product: dict[str, Any]) -> None:
    """Validate a Product object.

    Args:
        product: Product dict

    Raises:
        ValidationError: If validation fails
    """
    if "publisher_properties" in product and product["publisher_properties"]:
        for item in product["publisher_properties"]:
            validate_publisher_properties_item(item)

Validate a Product object.

Args
-----=
product
Product dict
Raises
-----=
ValidationError
If validation fails
def validate_publisher_properties_item(item: Any) ‑> None
Expand source code
def validate_publisher_properties_item(item: Any) -> None:
    """Validate a single ``publisher_properties[]`` entry.

    Accepts either a raw ``dict`` (the wire form) or a parsed Pydantic
    model instance (``PublisherPropertySelector1`` / ``…2`` / ``…3``).
    For Pydantic instances the model is coerced via
    ``.model_dump(exclude_none=False)`` and the same checks apply.

    Two XORs are enforced per the publisher-property-selector JSON Schema
    (adcp#4504):

    * Selector XOR: exactly one of ``property_ids`` / ``property_tags``
      is present for ``by_id`` / ``by_tag`` (``all`` requires neither).
    * Publisher XOR: exactly one of ``publisher_domain`` (singular) or
      ``publisher_domains`` (compact array) is present — both or neither
      both fail. ``publisher_domains`` is NOT allowed on
      ``selection_type='by_id'`` since property IDs are publisher-scoped;
      callers wanting per-publisher ID sets must use one entry per
      publisher.

    Why the Pydantic input form matters: ``datamodel-code-generator``
    cannot translate the JSON Schema's
    ``allOf[not[required[both]]] + anyOf[required[either]]`` construct
    into Pydantic field constraints, so the typed surface (selector 1/3
    direct instantiation) is laxer than the schema. Consumers parsing
    via Pydantic should call this helper post-construction to close the
    gap.

    Args:
        item: A single item from publisher_properties array — either a
            ``dict`` or a Pydantic ``BaseModel`` instance.

    Raises:
        ValidationError: If discriminator or field constraints are violated
    """
    if hasattr(item, "model_dump"):
        item = item.model_dump(exclude_none=False)
    if not isinstance(item, dict):
        raise ValidationError(
            "publisher_properties item must be a dict or a Pydantic model "
            f"instance, got {type(item).__name__}"
        )
    selection_type = item.get("selection_type")
    has_property_ids = "property_ids" in item and item["property_ids"] is not None
    has_property_tags = "property_tags" in item and item["property_tags"] is not None
    has_publisher_domain = "publisher_domain" in item and item["publisher_domain"] is not None
    publisher_domains = item.get("publisher_domains")
    has_publisher_domains = publisher_domains is not None

    if selection_type:
        if selection_type == "by_id" and not has_property_ids:
            raise ValidationError(
                "publisher_properties item with selection_type='by_id' must have property_ids"
            )
        elif selection_type == "by_tag" and not has_property_tags:
            raise ValidationError(
                "publisher_properties item with selection_type='by_tag' must have property_tags"
            )
        elif selection_type not in ("all", "by_id", "by_tag"):
            raise ValidationError(
                f"publisher_properties item has invalid selection_type: {selection_type}"
            )

    if has_property_ids and has_property_tags:
        raise ValidationError(
            "publisher_properties item cannot have both property_ids and property_tags. "
            "These fields are mutually exclusive."
        )

    # selection_type='all' carries neither selector array; older callers
    # without the discriminator must still provide one of the two.
    if selection_type not in ("all",) and not has_property_ids and not has_property_tags:
        raise ValidationError(
            "publisher_properties item must have either property_ids or property_tags. "
            "At least one is required."
        )

    if has_publisher_domain and has_publisher_domains:
        raise ValidationError(
            "publisher_properties item cannot have both publisher_domain and "
            "publisher_domains. These fields are mutually exclusive (XOR)."
        )

    if not has_publisher_domain and not has_publisher_domains:
        raise ValidationError(
            "publisher_properties item must have exactly one of publisher_domain "
            "or publisher_domains."
        )

    if has_publisher_domains and selection_type == "by_id":
        # by_id is single-publisher only — property IDs are publisher-scoped,
        # so fanning the same ID set across multiple publishers is meaningless.
        raise ValidationError(
            "publisher_properties item with selection_type='by_id' cannot use "
            "publisher_domains[]; property IDs are publisher-scoped. Use one "
            "entry per publisher with publisher_domain."
        )

    if has_publisher_domains:
        if not isinstance(publisher_domains, list) or len(publisher_domains) == 0:
            raise ValidationError(
                "publisher_properties item publisher_domains must be a non-empty array"
            )
        if any(not isinstance(d, str) or not d for d in publisher_domains):
            raise ValidationError(
                "publisher_properties item publisher_domains entries must be non-empty strings"
            )
        if len(set(publisher_domains)) != len(publisher_domains):
            raise ValidationError(
                "publisher_properties item publisher_domains entries must be unique"
            )

Validate a single publisher_properties[] entry.

Accepts either a raw dict (the wire form) or a parsed Pydantic model instance (PublisherPropertySelector1 / …2 / …3). For Pydantic instances the model is coerced via .model_dump(exclude_none=False) and the same checks apply.

Two XORs are enforced per the publisher-property-selector JSON Schema (adcp#4504):

  • Selector XOR: exactly one of property_ids / property_tags is present for by_id / by_tag (all requires neither).
  • Publisher XOR: exactly one of publisher_domain (singular) or publisher_domains (compact array) is present — both or neither both fail. publisher_domains is NOT allowed on selection_type='by_id' since property IDs are publisher-scoped; callers wanting per-publisher ID sets must use one entry per publisher.

Why the Pydantic input form matters: datamodel-code-generator cannot translate the JSON Schema's allOf[not[required[both]]] + anyOf[required[either]] construct into Pydantic field constraints, so the typed surface (selector 1/3 direct instantiation) is laxer than the schema. Consumers parsing via Pydantic should call this helper post-construction to close the gap.

Args
-----=
item
A single item from publisher_properties array — either a dict or a Pydantic BaseModel instance.
Raises
-----=
ValidationError
If discriminator or field constraints are violated
def validate_webhook_challenge_response(response: bytes | Mapping[str, Any],
*,
challenge: str,
field: str | None = None,
url: str | None = None) ‑> str
Expand source code
def validate_webhook_challenge_response(
    response: bytes | Mapping[str, Any],
    *,
    challenge: str,
    field: str | None = None,
    url: str | None = None,
) -> str:
    """Validate that a receiver echoed the challenge value.

    Receivers may respond with either ``{"challenge": "<value>"}`` or
    ``{"token": "<value>"}``. The return value is the field that matched.
    """

    try:
        if isinstance(response, bytes):
            decoded = json.loads(response.decode("utf-8"))
        else:
            decoded = dict(response)
    except (UnicodeDecodeError, json.JSONDecodeError, TypeError, ValueError) as exc:
        raise WebhookChallengeError(
            "webhook challenge response must be a JSON object",
            reason="invalid_json",
            field=field,
            url=url,
        ) from exc

    if not isinstance(decoded, Mapping):
        raise WebhookChallengeError(
            "webhook challenge response must be a JSON object",
            reason="invalid_json",
            field=field,
            url=url,
        )

    for key in ("challenge", "token"):
        value = decoded.get(key)
        if value == challenge:
            return key

    if "challenge" in decoded or "token" in decoded:
        reason = "challenge_mismatch"
        message = "webhook challenge response did not echo the expected value"
    else:
        reason = "missing_echo"
        message = "webhook challenge response must include 'challenge' or 'token'"
    raise WebhookChallengeError(message, reason=reason, field=field, url=url)

Validate that a receiver echoed the challenge value.

Receivers may respond with either {"challenge": "<value>"} or {"token": "<value>"}. The return value is the field that matched.

def verify_agent_authorization(adagents_data: dict[str, Any],
agent_url: str,
property_type: str | None = None,
property_identifiers: list[dict[str, str]] | None = None) ‑> bool
Expand source code
def verify_agent_authorization(
    adagents_data: dict[str, Any],
    agent_url: str,
    property_type: str | None = None,
    property_identifiers: list[dict[str, str]] | None = None,
) -> bool:
    """Check if agent is authorized for a property.

    Args:
        adagents_data: Parsed adagents.json data
        agent_url: URL of the sales agent to verify
        property_type: Type of property (website, app, etc.) - optional
        property_identifiers: List of identifiers to match - optional

    Returns:
        True if agent is authorized, False otherwise

    Raises:
        AdagentsValidationError: If adagents_data is malformed

    Notes:
        - If property_type/identifiers are None, checks if agent is authorized
          for ANY property on this domain
        - Implements AdCP domain matching rules
        - Agent URLs are matched ignoring protocol and trailing slash
    """
    # Validate structure
    if not isinstance(adagents_data, dict):
        raise AdagentsValidationError("adagents_data must be a dictionary")

    authorized_agents = adagents_data.get("authorized_agents")
    if not isinstance(authorized_agents, list):
        raise AdagentsValidationError("adagents.json must have 'authorized_agents' array")

    # Normalize the agent URL for comparison
    normalized_agent_url = normalize_url(agent_url)

    # Check each authorized agent
    for agent in authorized_agents:
        if not isinstance(agent, dict):
            continue

        agent_url_from_json = agent.get("url", "")
        if not agent_url_from_json:
            continue

        # Match agent URL (protocol-agnostic)
        if normalize_url(agent_url_from_json) != normalized_agent_url:
            continue

        # Found matching agent - now check properties
        properties = agent.get("properties")

        # If properties field is missing or empty, agent is authorized for all properties
        if properties is None or (isinstance(properties, list) and len(properties) == 0):
            return True

        # If no property filters specified, we found the agent - authorized
        if property_type is None and property_identifiers is None:
            return True

        # Check specific property authorization
        if isinstance(properties, list):
            for prop in properties:
                if not isinstance(prop, dict):
                    continue

                # Check property type if specified
                if property_type is not None:
                    prop_type = prop.get("property_type", "")
                    if prop_type != property_type:
                        continue

                # Check identifiers if specified
                if property_identifiers is not None:
                    prop_identifiers = prop.get("identifiers", [])
                    if not isinstance(prop_identifiers, list):
                        continue

                    if identifiers_match(property_identifiers, prop_identifiers):
                        return True
                else:
                    # Property type matched and no identifier check needed
                    return True

    return False

Check if agent is authorized for a property.

Args
-----=
adagents_data
Parsed adagents.json data
agent_url
URL of the sales agent to verify
property_type
Type of property (website, app, etc.) - optional
property_identifiers
List of identifiers to match - optional

Returns -----= True if agent is authorized, False otherwise

Raises
-----=
AdagentsValidationError
If adagents_data is malformed

Notes -----= - If property_type/identifiers are None, checks if agent is authorized for ANY property on this domain - Implements AdCP domain matching rules - Agent URLs are matched ignoring protocol and trailing slash

async def verify_agent_for_property(publisher_domain: str,
agent_url: str,
property_identifiers: list[dict[str, str]],
property_type: str | None = None,
timeout: float = 10.0,
client: httpx.AsyncClient | None = None) ‑> bool
Expand source code
async def verify_agent_for_property(
    publisher_domain: str,
    agent_url: str,
    property_identifiers: list[dict[str, str]],
    property_type: str | None = None,
    timeout: float = 10.0,
    client: httpx.AsyncClient | None = None,
) -> bool:
    """Convenience wrapper to fetch adagents.json and verify authorization in one call.

    Args:
        publisher_domain: Domain hosting the adagents.json file
        agent_url: URL of the sales agent to verify
        property_identifiers: List of identifiers to match
        property_type: Type of property (website, app, etc.) - optional
        timeout: Request timeout in seconds
        client: Optional httpx.AsyncClient for connection pooling

    Returns:
        True if agent is authorized, False otherwise

    Raises:
        AdagentsNotFoundError: If adagents.json not found (404)
        AdagentsValidationError: If JSON is invalid or malformed
        AdagentsTimeoutError: If request times out
    """
    adagents_data = await fetch_adagents(publisher_domain, timeout=timeout, client=client)
    return verify_agent_authorization(
        adagents_data=adagents_data,
        agent_url=agent_url,
        property_type=property_type,
        property_identifiers=property_identifiers,
    )

Convenience wrapper to fetch adagents.json and verify authorization in one call.

Args
-----=
publisher_domain
Domain hosting the adagents.json file
agent_url
URL of the sales agent to verify
property_identifiers
List of identifiers to match
property_type
Type of property (website, app, etc.) - optional
timeout
Request timeout in seconds
client
Optional httpx.AsyncClient for connection pooling

Returns -----= True if agent is authorized, False otherwise

Raises
-----=
AdagentsNotFoundError
If adagents.json not found (404)
AdagentsValidationError
If JSON is invalid or malformed
AdagentsTimeoutError
If request times out

Classes

class ADCPAuthenticationError (message: str, agent_id: str | None = None, agent_uri: str | None = None)
Expand source code
class ADCPAuthenticationError(ADCPError):
    """Authentication failed (401, 403).

    `is_retryable` defaults to ``False`` (inherited). Per the AdCP 3.0.4 prose
    tightening, `AUTH_REQUIRED` covers two sub-cases: credentials missing
    (correctable — supply credentials and retry) and credentials presented but
    rejected (terminal — re-presenting creates SSO retry-storm patterns).
    Defaulting to non-retryable is the safe biased-toward-the-dangerous-case
    choice; callers handling the missing-credentials case should retry only
    after attaching credentials, not on a timer. The 3.1 line splits this
    into `AUTH_MISSING` and `AUTH_INVALID`.
    """

    def __init__(self, message: str, agent_id: str | None = None, agent_uri: str | None = None):
        """Initialize authentication error."""
        suggestion = (
            "Check that your auth_token is valid and not expired.\n"
            "     Verify auth_type ('bearer' vs 'token') and auth_header are correct.\n"
            "     Some agents (like Optable) require auth_type='bearer' and "
            "auth_header='Authorization'"
        )
        super().__init__(message, agent_id, agent_uri, suggestion)

Authentication failed (401, 403).

is_retryable defaults to False (inherited). Per the AdCP 3.0.4 prose tightening, AUTH_REQUIRED covers two sub-cases: credentials missing (correctable — supply credentials and retry) and credentials presented but rejected (terminal — re-presenting creates SSO retry-storm patterns). Defaulting to non-retryable is the safe biased-toward-the-dangerous-case choice; callers handling the missing-credentials case should retry only after attaching credentials, not on a timer. The 3.1 line splits this into AUTH_MISSING and AUTH_INVALID.

Initialize authentication error.

Ancestors

  • ADCPError
  • builtins.Exception
  • builtins.BaseException

Inherited members

class ADCPClient (agent_config: AgentConfig,
webhook_url_template: str | None = None,
webhook_secret: str | None = None,
on_activity: Callable[[Activity], None] | None = None,
webhook_timestamp_tolerance: int = 300,
capabilities_ttl: float = 3600.0,
validate_features: bool = False,
strict_idempotency: bool = False,
signing: SigningConfig | None = None,
context_id: str | None = None,
validation: ValidationHookConfig | None = None,
force_a2a_version: str | None = None,
adcp_version: str | None = None,
server_version: str | None = None,
legacy_format_converter: LegacyFormatConverter | None = None,
canonical_format_legacy_resolver: CanonicalFormatLegacyResolver | None = None,
allow_unauthenticated_webhooks: bool = False)
Expand source code
class ADCPClient:
    """Client for interacting with a single AdCP agent."""

    def __init__(
        self,
        agent_config: AgentConfig,
        webhook_url_template: str | None = None,
        webhook_secret: str | None = None,
        on_activity: Callable[[Activity], None] | None = None,
        webhook_timestamp_tolerance: int = 300,
        capabilities_ttl: float = 3600.0,
        validate_features: bool = False,
        strict_idempotency: bool = False,
        signing: SigningConfig | None = None,
        context_id: str | None = None,
        validation: ValidationHookConfig | None = None,
        force_a2a_version: str | None = None,
        adcp_version: str | None = None,
        server_version: str | None = None,
        legacy_format_converter: LegacyFormatConverter | None = None,
        canonical_format_legacy_resolver: CanonicalFormatLegacyResolver | None = None,
        allow_unauthenticated_webhooks: bool = False,
    ):
        """
        Initialize ADCP client for a single agent.

        Args:
            agent_config: Agent configuration
            webhook_url_template: Template for webhook URLs with {agent_id},
                {task_type}, {operation_id}
            webhook_secret: Shared secret for the deprecated HMAC-SHA256 webhook
                fallback. Configure this only when the registration explicitly
                selected legacy HMAC; conformant public endpoints should use
                :class:`adcp.webhooks.WebhookReceiver` for RFC 9421 verification.
            allow_unauthenticated_webhooks: Explicit compatibility escape for
                accepting unsigned MCP webhooks when ``webhook_secret`` is not
                configured. Defaults to False so public webhook receivers fail
                closed. Only enable this for endpoints that cannot be reached
                from an untrusted network. A2A webhook handling is unaffected.
            on_activity: Callback for activity events
            webhook_timestamp_tolerance: Maximum age (in seconds) for webhook
                timestamps. Webhooks with timestamps older than this or more than
                this far in the future are rejected. Defaults to 300 (5 minutes).
            capabilities_ttl: Time-to-live in seconds for cached capabilities (default: 1 hour)
            validate_features: When True, automatically check that the seller supports
                required features before making task calls (e.g., sync_audiences requires
                audience_targeting). Requires capabilities to have been fetched first.
            strict_idempotency: When True, verify the seller declared
                ``adcp.idempotency.replay_ttl_seconds`` in capabilities before any
                mutating call. Fetches capabilities lazily on first use. Raises
                ``IdempotencyUnsupportedError`` if the declaration is missing —
                sellers that don't declare it provide no retry-safety guarantee
                per AdCP #2315. Defaults to False for backward compatibility.
            signing: Optional RFC 9421 request-signing config. When provided,
                the client automatically attaches ``Signature`` /
                ``Signature-Input`` / ``Content-Digest`` headers to operations
                the seller's ``request_signing`` capability lists in
                ``required_for``, ``warn_for``, or ``supported_for``. The
                seller's ``covers_content_digest`` policy determines whether
                the body is bound to the signature. Generate a key with
                ``adcp-keygen`` and publish the public JWK at your
                ``jwks_uri``. Supported on both A2A and MCP
                (``mcp_transport="streamable_http"``); SSE-transport MCP
                logs a warning and falls through unsigned.
            validation: Schema-driven validation modes for outgoing
                requests and incoming responses against the bundled AdCP
                JSON schemas. Defaults (matching the TS port): requests
                in ``warn`` mode (drift logged but not blocked — partial
                payloads in error-path tests still work) and responses
                in ``strict`` mode (agent drift fails the task).
                ``ADCP_VALIDATION_MODE=strict|warn|off`` overrides both
                sides at call time (matches the TS port); ``ADCP_ENV``
                set to ``production`` / ``prod`` flips only the response
                default to ``warn``. Generic ``ENV`` / ``ENVIRONMENT`` /
                ``PYTHON_ENV`` are deliberately ignored — they collide
                with unrelated tooling. Storyboards and compliance
                runners that want hard-stop enforcement everywhere pass
                ``validation=ValidationHookConfig(requests="strict",
                responses="strict")``; high-throughput callers can set
                either side to ``"off"`` to skip the validator entirely
                with zero overhead.
            context_id: A2A-only. Seed the A2A conversation context. Pass a
                previously-returned ``context_id`` to resume a session
                across process restarts, or a self-assigned UUID to name
                the session with your own correlation key (the ADK server
                honors buyer-proposed ids). If omitted, the server mints
                one on the first message and this client auto-retains it
                for subsequent calls. Read the current value via
                ``client.context_id``; call ``client.reset_context()`` to
                start a fresh conversation. Rule of thumb: one
                ``ADCPClient`` per A2A conversation — if a buyer has
                multiple concurrent briefs with the same agent, construct
                one client per brief rather than sharing.

                For HITL flows that can span a process restart mid-task,
                use ``checkpoint()`` / ``from_checkpoint()`` instead of
                persisting ``context_id`` alone — full resume state is
                both ``context_id`` AND ``active_task_id``.

                Raises ``TypeError`` if passed with a non-A2A protocol.
            force_a2a_version: A2A-only. Pin the **A2A transport
                version** (e.g. ``"0.3"``, ``"1.0"``) by filtering the
                peer's advertised ``supported_interfaces`` to entries
                whose ``protocol_version`` matches. Not for AdCP
                protocol pinning — see ``adcp_version`` for that.
                Intended for tests or for forcing a 0.3-speaking path
                against a dual-advertising peer. Raises
                :class:`ADCPConnectionError` on the first call if no
                advertised interface matches. ``None`` (default) lets
                the SDK's ``ClientFactory`` pick the most capable
                transport the peer supports. Use
                :attr:`a2a_protocol_versions` to probe what a peer
                advertises before pinning.

                Raises ``TypeError`` if passed with a non-A2A protocol.
            adcp_version: AdCP protocol release this client speaks
                (release-precision string, e.g. ``"3.0"``, ``"3.1"``,
                ``"3.1-beta"``). Stripe-style per-instance pin: the
                value is sent as ``adcp_version`` on every outbound
                request and selects creative dialect behavior. ``None``
                (default) resolves
                to the SDK's compile-time pin (``ADCP_VERSION``
                packaged with the wheel). Cross-major pins raise
                :class:`ConfigurationError` at construction; install
                the SDK major that targets your wire version instead.
                Patch-precision strings (``"3.0.1"``) and build
                metadata (``"3.0.1+canary"``) are accepted at construction
                but normalized to release-precision before wire emission
                per the spec — patches and build metadata are not part
                of the negotiation contract. ``get_adcp_version()``
                returns the normalized form.

                Caller-supplied ``adcp_version`` on a per-call params
                dict wins over the constructor pin: the enricher is
                the default, not an override.

                Migration from ``adcp_major_version`` (legacy integer
                wire field): generated request types still expose
                ``adcp_major_version: int | None`` from the pre-#3493
                schema. Both fields will coexist on the wire through
                3.x; servers prefer the new ``adcp_version`` when both
                are present. Stop populating ``adcp_major_version`` on
                request models once your seller advertises 3.1 in
                ``supported_versions``.
            server_version: AdCP wire shape the *seller* speaks. Most
                adopters leave this ``None`` — the SDK assumes a v3
                seller and the seller's
                ``/.well-known/agent-card.json`` is the canonical
                source of truth once a probe-and-cache path lands.

                Pin explicitly when:

                * You're talking to a known-legacy seller (e.g.
                  ``server_version="3.0"``). Canonical discovery results
                  are upgraded for application code and canonical writes
                  are downgraded only through preserved or explicit routes.
                * You want telemetry to attribute outbound traffic to
                  a specific server-side version regardless of what the
                  seller advertises.

                Retrieve the current value via :meth:`get_server_version`.
        """
        self._adcp_version: str = resolve_adcp_version(adcp_version)
        self._server_version: str | None = _resolve_server_version(server_version)
        if type(allow_unauthenticated_webhooks) is not bool:
            raise TypeError("allow_unauthenticated_webhooks must be a bool")

        self.agent_config = agent_config
        self.webhook_url_template = webhook_url_template
        self.webhook_secret = webhook_secret
        self.allow_unauthenticated_webhooks = allow_unauthenticated_webhooks
        self.on_activity = on_activity
        self.webhook_timestamp_tolerance = webhook_timestamp_tolerance
        self.capabilities_ttl = capabilities_ttl
        self.validate_features = validate_features
        self.strict_idempotency = strict_idempotency
        self.signing = signing
        if (
            signing is not None
            and agent_config.agent_uri.startswith("http://")
            and not is_loopback_http_uri(agent_config.agent_uri)
        ):
            raise ValueError(
                "request signing requires an https:// agent_uri for non-loopback hosts; "
                "plain HTTP is only allowed for localhost/loopback development"
            )
        self.legacy_format_converter = legacy_format_converter
        self.canonical_format_legacy_resolver = canonical_format_legacy_resolver
        self._canonical_legacy_routes: dict[
            tuple[str | None, str | None, str], list[dict[str, Any]] | None
        ] = {}

        # Capabilities cache
        self._capabilities: GetAdcpCapabilitiesResponse | None = None
        self._feature_resolver: FeatureResolver | None = None
        self._capabilities_fetched_at: float | None = None
        self._idempotency_capability_verified: bool = False
        # Unique per-instance token so use_idempotency_key scopes to this
        # client and does not bleed to siblings (AdCP #2315 cross-seller risk).
        from uuid import uuid4 as _uuid4

        self._idempotency_client_token: str = _uuid4().hex

        if force_a2a_version is not None and agent_config.protocol != Protocol.A2A:
            raise TypeError(
                f"force_a2a_version is only supported for A2A protocol; got {agent_config.protocol}"
            )

        # Initialize protocol adapter
        self.adapter: ProtocolAdapter
        if agent_config.protocol == Protocol.A2A:
            self.adapter = A2AAdapter(agent_config, force_a2a_version=force_a2a_version)
        elif agent_config.protocol == Protocol.MCP:
            self.adapter = MCPAdapter(agent_config)
        else:
            raise ValueError(f"Unsupported protocol: {agent_config.protocol}")

        self.adapter.idempotency_client_token = self._idempotency_client_token
        if strict_idempotency:
            self.adapter.idempotency_capability_check = self._ensure_idempotency_capability
        if signing is not None:
            self.adapter.signing_request_hook = self._sign_outgoing_request
            self.adapter.signing_capability_check = self._prepare_signing_capabilities
        # Apply schema validation modes (default: requests=warn, responses=strict
        # in dev/test, warn in production — see ``ValidationHookConfig`` docs).
        self.adapter.configure_validation(validation)
        # Auto-inject the per-instance ``adcp_version`` pin into every
        # outbound request envelope. Caller-supplied values on the
        # request object win — the enricher is the default, not an
        # override — so per-call overrides remain available once the
        # generated request types declare the field.
        _pinned_version = self._server_version or self._adcp_version
        self._signing_profile_version = (
            None
            if signing is None
            else signing.signing_profile_version
            or signing_profile_for_adcp_version(_pinned_version)
        )

        def _inject_adcp_version(params: dict[str, Any]) -> dict[str, Any]:
            return {"adcp_version": _pinned_version, **params}

        self.adapter.envelope_enricher = _inject_adcp_version

        if context_id:
            # Empty string is treated as "not provided" — callers using
            # ``context_id=os.getenv("...") or ""`` patterns shouldn't
            # silently seed an empty id on the wire.
            if not isinstance(self.adapter, A2AAdapter):
                raise TypeError(
                    f"context_id is only supported for A2A protocol; got {agent_config.protocol}"
                )
            self.adapter.set_context_id(context_id)

        # Initialize simple API accessor (lazy import to avoid circular dependency)
        from adcp.simple import SimpleAPI

        self.simple = SimpleAPI(self)

    def get_adcp_version(self) -> str:
        """Return the AdCP protocol release this client is pinned to.

        Resolved at construction from the ``adcp_version`` kwarg, with
        fallback to the SDK's compile-time pin (``ADCP_VERSION``
        packaged with the wheel) when the caller didn't pin
        explicitly. Same value across the client's lifetime — the pin
        is per-instance, not per-call.

        See ``__init__``'s ``adcp_version`` parameter for the full
        semantics, including the cross-major fence and dialect selection.
        """
        return self._adcp_version

    def get_server_version(self) -> str | None:
        """Return the seller's AdCP wire-shape version, or ``None``.

        ``None`` means the SDK is assuming a current-major seller
        (the default). Returns a release-precision string
        (``"3.0"``, ``"3.1"``, ``"2.5"``) when the adopter pinned
        via the ``server_version`` constructor arg or — once the
        agent-card probe lands — when the SDK detected the seller's
        version from its agent-card.

        See ``__init__``'s ``server_version`` parameter for negotiated
        canonical/legacy creative behavior.
        """
        return self._server_version

    @property
    def context_id(self) -> str | None:
        """Current A2A conversation context_id.

        Reads the context_id currently associated with this client: the
        value assigned by the A2A server (auto-captured from the most
        recent response) or the one seeded via the constructor or
        ``reset_context()``. Returns ``None`` before the first A2A call
        in a fresh conversation, or for clients on non-A2A protocols —
        reads are lenient across protocols so generic code can probe
        ``if client.context_id: ...`` safely. Writes (constructor kwarg,
        ``reset_context``) raise on non-A2A because the operation has no
        meaning there.

        Not safe for concurrent calls on the same client — the adapter
        mutates this on every response. Rule of thumb: one ADCPClient
        per A2A conversation.

        For simple completed-task resume, persist this value and pass
        it to ``ADCPClient(context_id=...)``. For HITL flows that may
        restart mid-``input-required``, use ``checkpoint()`` /
        ``from_checkpoint()`` — full resume state is both this id AND
        ``active_task_id``.
        """
        if isinstance(self.adapter, A2AAdapter):
            return self.adapter.context_id
        return None

    @property
    def active_task_id(self) -> str | None:
        """A2A task_id the next send must echo to resume the same task.

        Set when the last A2A response was non-terminal
        (``input-required``, ``working``, ``submitted``,
        ``auth-required``). The adapter echoes this id on the next
        outbound message so the server resumes the same task. Clears
        automatically when the task reaches a terminal state.

        Full resume state is *both* ``context_id`` and
        ``active_task_id`` — persist both (or use ``checkpoint()``) to
        survive a process restart mid-HITL without orphaning the task.

        Returns ``None`` for non-A2A clients.
        """
        if isinstance(self.adapter, A2AAdapter):
            return self.adapter.active_task_id
        return None

    @property
    def a2a_protocol_versions(self) -> list[str] | None:
        """A2A ``protocol_version`` strings the peer advertises, sorted.

        Lazily populated after the first operation that fetches the
        peer's ``AgentCard`` (``fetch_capabilities``, ``list_tools``,
        ``get_agent_info``, or any skill-call). Returns ``None`` before
        the card has been fetched so callers can distinguish "not yet
        known" from "peer advertises nothing" (empty list). Returns
        ``None`` for non-A2A clients.

        Useful for probing which wire version a peer speaks — buyers
        running alongside both 0.3-era and 1.0-era agents can use this
        to confirm what they're talking to.
        """
        if isinstance(self.adapter, A2AAdapter):
            return self.adapter.a2a_protocol_versions
        return None

    def reset_context(self, context_id: str | None = None) -> None:
        """Start a new A2A conversation on this client.

        Passing ``None`` (default) clears the current context so the
        server mints a fresh one on the next call. Passing a string uses
        it as the new conversation id — useful for resuming a specific
        prior session or for naming the conversation with your own
        correlation key. Note: some servers (notably ADK) rewrite
        client-supplied ids into their own session format; the client
        auto-adopts the rewritten id on the next response.

        Also clears any active_task_id — starting a new conversation
        discards any in-flight task on the old one.

        Raises ``TypeError`` when called on a non-A2A client.
        """
        if not isinstance(self.adapter, A2AAdapter):
            raise TypeError(
                f"reset_context is only supported for A2A protocol; "
                f"got {self.agent_config.protocol}"
            )
        self.adapter.set_context_id(context_id)

    def checkpoint(self) -> Checkpoint:
        """Return the minimal state needed to resume this A2A session.

        Full resume for HITL / multi-turn flows requires *both*
        ``context_id`` (which conversation) AND ``active_task_id``
        (which in-flight task to echo). Persisting only ``context_id``
        reconnects to the right conversation but orphans the pending
        task server-side — the next send starts a new task under the
        same context, and the original ``input-required`` task is
        abandoned.

        The returned dict also carries ``agent_id`` so a later
        ``from_checkpoint`` call against a different ``AgentConfig``
        fails loudly instead of sending one agent's session ids to
        another.

        Pair with ``ADCPClient.from_checkpoint(agent_config, state)``.

        Returns a fully-populated ``Checkpoint`` on non-A2A clients
        with ``context_id``/``active_task_id`` set to ``None``, so
        generic persist-and-restore code can call this without
        branching on protocol.
        """
        return Checkpoint(
            agent_id=self.agent_config.id,
            context_id=self.context_id,
            active_task_id=self.active_task_id,
        )

    @classmethod
    def from_checkpoint(
        cls,
        agent_config: AgentConfig,
        state: Checkpoint,
        **kwargs: Any,
    ) -> ADCPClient:
        """Rehydrate an ADCPClient from a prior ``checkpoint()``.

        Restores both ``context_id`` and ``active_task_id`` so a process
        restart mid-``input-required`` can resume the same task, not
        orphan it. Accepts the same keyword arguments as ``__init__``
        (signing, strict_idempotency, etc.) — the checkpoint only
        carries session-resume state; operational config is re-supplied
        by the caller.

        Raises ``ValueError`` if the checkpoint's ``agent_id`` doesn't
        match ``agent_config.id`` — a checkpoint minted for Agent A
        must not be restored onto Agent B, or the client will leak
        Agent A's opaque session ids to Agent B on the next message.

        Raises ``TypeError`` on a non-A2A ``agent_config`` if the
        checkpoint carries a non-empty ``context_id`` or
        ``active_task_id`` — session-resume state on a protocol that
        doesn't support it would be silently dropped, masking bugs.
        An empty/absent checkpoint round-trips cleanly on any protocol.
        """
        saved_agent_id = state.get("agent_id") if state else None
        if saved_agent_id and saved_agent_id != agent_config.id:
            raise ValueError(
                f"checkpoint was minted for agent {saved_agent_id!r}, "
                f"cannot restore against {agent_config.id!r}"
            )
        context_id = state.get("context_id") if state else None
        active_task_id = state.get("active_task_id") if state else None
        if active_task_id and agent_config.protocol != Protocol.A2A:
            raise TypeError(
                f"active_task_id in checkpoint is only supported for A2A "
                f"protocol; got {agent_config.protocol}"
            )
        client = cls(agent_config, context_id=context_id, **kwargs)
        if active_task_id and isinstance(client.adapter, A2AAdapter):
            client.adapter._restore_active_task_id(active_task_id)
        return client

    @classmethod
    def from_mcp_client(
        cls,
        client: ClientSession,
        *,
        agent_id: str | None = None,
        validation: ValidationHookConfig | None = None,
        capabilities_ttl: float = 3600.0,
        validate_features: bool = False,
        strict_idempotency: bool = False,
    ) -> ADCPClient:
        """Create an ADCPClient wrapping a pre-connected MCP ClientSession.

        Parity with JS ``AgentClient.fromMCPClient()`` (v5.19.0). The primary
        use case is compliance test fleets that wire a full ``ADCPClient``
        against an in-process MCP server without standing up a loopback HTTP
        server.

        Warning:
            The returned client's ``close()`` and ``async with`` ``__aexit__``
            are **no-ops** — the caller owns the injected session and is
            responsible for closing it. Code that relies on ``async with
            ADCPClient.from_mcp_client(...) as c:`` to clean up the session
            will leak the session.

            Webhook delivery and ``on_activity`` callbacks are **not wired**
            on the in-process path — there is no HTTP transport for the
            seller to call back through. Don't pass these to the factory
            (they're absent from the signature on purpose).

            If the injected session has not been initialized
            (``await session.initialize()``), the first tool call surfaces
            as an opaque MCP protocol error in ``TaskResult.error``. The
            factory does not initialize for you — verify before calling.

        **Session lifecycle:** the caller owns the session — ``close()`` and
        ``async with`` exit on the returned client are no-ops. Use your own
        ``AsyncExitStack`` to scope both the transport and the client::

            import contextlib
            from mcp import ClientSession
            from mcp.shared.memory import create_client_server_memory_streams

            async with contextlib.AsyncExitStack() as stack:
                (c_read, c_write), (s_read, s_write) = await stack.enter_async_context(
                    create_client_server_memory_streams()
                )
                # wire your in-process server to (s_read, s_write) here
                session = await stack.enter_async_context(
                    ClientSession(c_read, c_write)
                )
                await session.initialize()
                # close() is a no-op on injected sessions; no stack.enter_async_context needed.
                adcp_client = ADCPClient.from_mcp_client(session, agent_id="test-seller")
                result = await adcp_client.get_products(GetProductsRequest(...))

        Note:
            Request signing is not supported on the injected-session path —
            the signing hook is wired into the HTTP transport layer that is
            bypassed here. ``signing=`` is intentionally absent from this
            factory's parameters.

        Args:
            client: A pre-connected ``mcp.ClientSession`` whose
                ``initialize()`` has already been awaited.
            agent_id: Identifier for the wrapped agent used in log messages
                and error objects. Defaults to a unique ``in-process-XXXXXXXX``
                token; set this explicitly when running multiple in-process
                agents concurrently so log lines are distinguishable.
            validation: Schema-validation modes (same as ``__init__``).
            strict_idempotency: Verify seller declared idempotency support
                before each mutating call (same as ``__init__``).
            validate_features: Gate tool calls on fetched capability
                declarations (same as ``__init__``).
            capabilities_ttl: TTL for the capability cache in seconds
                (same as ``__init__``).

        Returns:
            A fully configured ``ADCPClient`` backed by the injected session.
        """
        effective_id = agent_id if agent_id is not None else f"in-process-{uuid4().hex[:8]}"
        config = AgentConfig(
            id=effective_id,
            # RFC 2606 .invalid TLD — passes the http:// validator, guaranteed
            # not to route to a real host. Self-documenting in error messages.
            agent_uri="http://in-process.invalid",
            protocol=Protocol.MCP,
        )
        instance = cls(
            config,
            validation=validation,
            strict_idempotency=strict_idempotency,
            validate_features=validate_features,
            capabilities_ttl=capabilities_ttl,
        )
        if not isinstance(instance.adapter, MCPAdapter):
            raise RuntimeError(  # pragma: no cover
                f"from_mcp_client: expected MCPAdapter but got {type(instance.adapter).__name__}"
            )
        instance.adapter._inject_session(client)
        return instance

    async def _ensure_idempotency_capability(self) -> None:
        """Verify the seller positively declares idempotency support in capabilities.

        Called before every mutating request when ``strict_idempotency=True``.
        Fetches capabilities on first invocation; subsequent calls are no-ops
        once the declaration has been observed. Raises
        ``IdempotencyUnsupportedError`` when ``adcp.idempotency`` is missing,
        declares ``supported=False`` (seller does not dedupe — naive retry
        would double-process), or declares ``supported=True`` without a
        ``replay_ttl_seconds`` window.

        Sets ``_idempotency_capability_verified = True`` BEFORE calling
        ``fetch_capabilities`` so any recursive dispatch through the adapter
        terminates (``get_adcp_capabilities`` is non-mutating, so it would
        short-circuit anyway — but this guard protects against future refactors
        that might add it to the mutating set).
        """
        from adcp.exceptions import IdempotencyUnsupportedError

        if self._idempotency_capability_verified:
            return

        self._idempotency_capability_verified = True
        try:
            caps = await self.fetch_capabilities()
            adcp_info = getattr(caps, "adcp", None)
            idempotency_info = getattr(adcp_info, "idempotency", None) if adcp_info else None

            if idempotency_info is None:
                raise IdempotencyUnsupportedError(
                    agent_id=self.agent_config.id,
                    agent_uri=self.agent_config.agent_uri,
                    reason="seller did not declare adcp.idempotency",
                )

            supported = getattr(idempotency_info, "supported", None)
            if supported is False:
                raise IdempotencyUnsupportedError(
                    agent_id=self.agent_config.id,
                    agent_uri=self.agent_config.agent_uri,
                    reason="seller declared adcp.idempotency.supported=false",
                )

            ttl = getattr(idempotency_info, "replay_ttl_seconds", None)
            if ttl is None:
                raise IdempotencyUnsupportedError(
                    agent_id=self.agent_config.id,
                    agent_uri=self.agent_config.agent_uri,
                    reason=(
                        "seller declared adcp.idempotency.supported=true but omitted "
                        "replay_ttl_seconds"
                    ),
                )
        except Exception:
            self._idempotency_capability_verified = False
            raise

    async def _prepare_signing_capabilities(self) -> None:
        """Populate signing policy before a transport writer sends a request."""
        await self.fetch_capabilities()

    @staticmethod
    def _mcp_operation_from_request(request: httpx.Request) -> str | None:
        """Extract one MCP ``tools/call`` name from its JSON-RPC body."""
        try:
            payload = json.loads(request.content)
        except (json.JSONDecodeError, TypeError, UnicodeDecodeError):
            return None
        if not isinstance(payload, dict) or payload.get("method") != "tools/call":
            return None
        params = payload.get("params")
        if not isinstance(params, dict):
            return None
        name = params.get("name")
        return name if isinstance(name, str) and name else None

    async def _sign_outgoing_request(self, request: httpx.Request) -> None:
        """httpx request event hook that attaches RFC 9421 signature headers.

        Installed on the protocol adapter's httpx client when a
        ``SigningConfig`` was passed to ``ADCPClient``. Consults the
        seller's advertised ``request_signing`` capability and signs only
        the operations the seller listed in ``required_for``, ``warn_for``,
        or ``supported_for`` — other requests (including the agent-card
        fetch and ``get_adcp_capabilities`` itself) pass through unsigned.
        The ``covers_content_digest`` tri-state determines whether the
        body is bound to the signature.
        """
        if self.signing is None:
            return
        mcp_operation = self._mcp_operation_from_request(request)
        operation = mcp_operation or _signing_current_operation.get()
        # Unset ContextVar → out-of-band call (agent-card fetch, session
        # initialize, etc). Skip without fetching capabilities.
        #
        # get_adcp_capabilities → bootstrap carve-out: signing it would
        # require capabilities we don't have yet, and if a pathological
        # seller listed this op in its own required_for we'd recurse.
        # Keep this check narrow — only operations strictly required to
        # *obtain* capabilities belong here. Today that's just
        # get_adcp_capabilities. A future adapter that adds another
        # capabilities-precondition op MUST extend this guard.
        if operation is None or operation == "get_adcp_capabilities":
            return

        if mcp_operation is not None:
            # MCP's httpx hook runs in a writer task whose ContextVar snapshot
            # was captured when the session connected. The adapter prefetches
            # capabilities in the caller task before enqueueing tools/call;
            # fetching here would deadlock the same writer stream.
            caps = self._capabilities
            if caps is None:
                raise RuntimeError(
                    "MCP request signing policy was not prefetched before tools/call"
                )
        else:
            # A2A's event hook runs in the caller task. Retain this fallback
            # for direct/custom transports, while the bundled adapter also
            # prefetches so normal hooks stay network-free.
            caps = self._capabilities or await self.fetch_capabilities()
        req_signing = getattr(caps, "request_signing", None)

        # Detect and surface a malformed seller config: supported=False is
        # "signatures are ignored", but populating required_for alongside
        # it is contradictory. The classifier correctly skips (matches
        # verifier behavior) but the silent downgrade hides a config bug
        # that will bite pilots.
        if (
            req_signing is not None
            and not req_signing.supported
            and (req_signing.required_for or req_signing.warn_for)
        ):
            logger.warning(
                "Seller %s advertises request_signing.supported=false but "
                "populates required_for/warn_for — treating as unsupported "
                "per spec. Verify the seller's capability advertisement.",
                self.agent_config.id,
            )

        decision = operation_needs_signing(req_signing, operation)
        if decision == "skip":
            return

        covers_policy: str | None = None
        if req_signing is not None and req_signing.covers_content_digest is not None:
            covers_policy = req_signing.covers_content_digest.value
        if covers_policy == "forbidden":
            cover_digest = False
        elif covers_policy == "required":
            cover_digest = True
        else:
            # "either" or absent — signer's choice; default stricter.
            cover_digest = True

        body = request.content
        signing_profile_version = self._signing_profile_version
        if signing_profile_version is None:  # pragma: no cover - constructor invariant
            raise RuntimeError("request signing profile was not initialized")
        signed = sign_request(
            method=request.method,
            url=str(request.url),
            headers=dict(request.headers),
            body=body,
            private_key=self.signing.private_key,
            key_id=self.signing.key_id,
            alg=self.signing.alg,
            cover_content_digest=cover_digest,
            tag=self.signing.tag,
            signing_profile_version=signing_profile_version,
        )
        # pop-then-set ensures our signed values are authoritative even if
        # another hook or earlier layer added a same-named header. httpx
        # headers are a case-insensitive MultiDict, so a naive assignment
        # could leave a duplicate value in a different case.
        for header_name, header_value in signed.as_dict().items():
            request.headers.pop(header_name, None)
            request.headers[header_name] = header_value

    def get_webhook_url(self, task_type: str, operation_id: str) -> str:
        """Generate webhook URL for a task."""
        if not self.webhook_url_template:
            raise ValueError("webhook_url_template not configured")

        return self.webhook_url_template.format(
            agent_id=self.agent_config.id,
            task_type=task_type,
            operation_id=operation_id,
        )

    def _emit_activity(self, activity: Activity) -> None:
        """Emit activity event."""
        if self.on_activity:
            self.on_activity(activity)

    @contextlib.contextmanager
    def use_idempotency_key(self, key: str) -> Iterator[str]:
        """Pin an ``idempotency_key`` for the next mutating call on THIS client.

        Use when you've persisted a key (e.g., in a buyer-side database) and
        want the SDK to send that exact key on resume or retry across process
        restarts. The key is validated against ``^[A-Za-z0-9_.:-]{16,255}$`` on
        entry; a ``ValueError`` is raised for malformed keys.

        Scope rules:

        * **Single-use within scope.** The first mutating call inside the
          ``with`` block consumes the pinned key; a second mutating call falls
          through to a fresh UUID. This protects against ``asyncio.gather``
          siblings accidentally sharing the key (which would trigger
          ``IDEMPOTENCY_CONFLICT`` or silently duplicate work). If you need to
          retry, wrap each attempt in its own ``with`` block.
        * **Client-scoped.** The pinned key applies only to calls on THIS
          client. A mutating call on a sibling ``ADCPClient`` inside the same
          ``with`` block generates a fresh key and emits a ``UserWarning`` —
          keys must be unique per (seller, request) pair (AdCP #2315).
        * **No nesting.** Nested ``use_idempotency_key`` on the same client
          raises ``RuntimeError``.

        Example::

            with client.use_idempotency_key(campaign.stored_key):
                result = await client.create_media_buy(request)
        """
        from adcp import _idempotency

        _idempotency.validate_key(key)
        token = self._idempotency_client_token
        if token in _idempotency._scoped_keys:
            raise RuntimeError(
                "use_idempotency_key is already active on this client; "
                "nested usage is not supported."
            )
        _idempotency._scoped_keys[token] = key
        try:
            yield key
        finally:
            _idempotency._scoped_keys.pop(token, None)

    # ========================================================================
    # Capability Validation
    # ========================================================================

    @property
    def capabilities(self) -> GetAdcpCapabilitiesResponse | None:
        """Return cached capabilities, or None if not yet fetched."""
        return self._capabilities

    @property
    def feature_resolver(self) -> FeatureResolver | None:
        """Return the FeatureResolver for cached capabilities, or None."""
        return self._feature_resolver

    async def fetch_capabilities(self) -> GetAdcpCapabilitiesResponse:
        """Fetch capabilities, using cache if still valid.

        Returns:
            The seller's capabilities response.
        """
        if self._capabilities is not None and self._capabilities_fetched_at is not None:
            elapsed = time.monotonic() - self._capabilities_fetched_at
            if elapsed < self.capabilities_ttl:
                return self._capabilities

        return await self.refresh_capabilities()

    async def refresh_capabilities(self) -> GetAdcpCapabilitiesResponse:
        """Fetch capabilities from the seller, bypassing cache.

        On strict-schema validation failure the raw response is inspected with
        ``looks_like_v3_capabilities``: if the agent is structurally v3-shaped,
        a wire-shape bug is surfaced loudly with the original validation error
        rather than silently downgrading to v2 (the v2 fallback would then ask
        for v2.5 schemas, which aren't shipped — one missing field would
        cascade into "AdCP schema data for version v2.5 not found"). Genuinely
        non-v3 responses still fall through to the transport-error path.

        Returns:
            The seller's capabilities response.

        Raises:
            ADCPError: On transport failure, or when the response is
                v3-shaped but fails schema validation. The error message
                explicitly references v3 in the latter case so the underlying
                wire-shape bug doesn't get blamed on a v2.5-schema cascade.
        """
        result = await self.get_adcp_capabilities(GetAdcpCapabilitiesRequest())
        if result.success and result.data is not None:
            self._capabilities = result.data
            self._feature_resolver = FeatureResolver(result.data)
            self._capabilities_fetched_at = time.monotonic()
            return self._capabilities

        # The typed call discards the raw payload on parse failure (only the
        # error string survives). Distinguish parse-failure (worth shape-
        # checking) from transport-failure (no data ever arrived) by the
        # error prefix produced by ProtocolAdapter._parse_response. Only on
        # parse-failure do we re-fetch the raw dict from the adapter to
        # inspect its shape; transport failures fall straight through to
        # the original error path.
        raw_data: Any = None
        is_parse_failure = result.error is not None and result.error.startswith(
            "Failed to parse response:"
        )
        if is_parse_failure:
            raw_result = await self.adapter.get_adcp_capabilities(
                GetAdcpCapabilitiesRequest().model_dump(mode="json", exclude_none=True)
            )
            raw_data = raw_result.data
            if isinstance(raw_data, list) and len(raw_data) == 1 and isinstance(raw_data[0], dict):
                # MCP content array — unwrap a single-item content envelope
                # so the heuristic sees the same shape the parser would.
                raw_data = raw_data[0]

        if looks_like_v3_capabilities(raw_data):
            logger.warning(
                "[AdCP] Agent %r returned a get_adcp_capabilities response that "
                "failed validation, but the response is structurally v3-shaped. "
                "The agent has a wire-shape bug — that's the thing to fix. "
                "(has_error=%s, has_data=%s)",
                self.agent_config.id,
                bool(result.error),
                raw_data is not None,
            )
            raise ADCPError(
                f"v3 capabilities response from agent {self.agent_config.id!r} "
                f"failed schema validation: {result.error or result.message}. "
                f"The response is structurally v3-shaped (carries `adcp`, "
                f"`supported_protocols`, or a v3 protocol block) — fix the "
                f"agent's wire shape rather than downgrading to v2.",
                agent_id=self.agent_config.id,
                agent_uri=self.agent_config.agent_uri,
            )

        raise ADCPError(
            f"Failed to fetch capabilities: {result.error or result.message}",
            agent_id=self.agent_config.id,
            agent_uri=self.agent_config.agent_uri,
        )

    def _ensure_resolver(self) -> FeatureResolver:
        """Return the FeatureResolver, raising if capabilities haven't been fetched."""
        if self._feature_resolver is None:
            raise ADCPError(
                "Cannot check feature support: capabilities have not been fetched. "
                "Call fetch_capabilities() first.",
                agent_id=self.agent_config.id,
                agent_uri=self.agent_config.agent_uri,
            )
        return self._feature_resolver

    def supports(self, feature: str) -> bool:
        """Check if the seller supports a feature.

        Supports multiple feature namespaces:
        - Protocol support: ``supports("media_buy")`` checks ``supported_protocols``
        - Extension support: ``supports("ext:scope3")`` checks ``extensions_supported``
        - Targeting: ``supports("targeting.geo_countries")`` checks
          ``media_buy.execution.targeting``
        - Media buy features: ``supports("audience_targeting")`` checks
          ``media_buy.features``
        - Signals features: ``supports("catalog_signals")`` checks
          ``signals.features``

        Args:
            feature: Feature identifier to check.

        Returns:
            True if the seller declares the feature as supported.

        Raises:
            ADCPError: If capabilities have not been fetched yet.
        """
        return self._ensure_resolver().supports(feature)

    def require(self, *features: str) -> None:
        """Assert that the seller supports all listed features.

        Args:
            *features: Feature identifiers to require.

        Raises:
            ADCPFeatureUnsupportedError: If any features are not supported.
            ADCPError: If capabilities have not been fetched yet.
        """
        self._ensure_resolver().require(
            *features,
            agent_id=self.agent_config.id,
            agent_uri=self.agent_config.agent_uri,
        )

    def _validate_task_features(self, task_name: str) -> None:
        """Check feature requirements for a task if validate_features is enabled.

        Returns without checking if validate_features is False or capabilities
        haven't been fetched yet (logs a warning in the latter case).
        """
        if not self.validate_features:
            return
        if self._feature_resolver is None:
            logger.warning(
                "validate_features is enabled but capabilities have not been fetched. "
                "Call fetch_capabilities() to enable feature validation."
            )
            return
        required_feature = TASK_FEATURE_MAP.get(task_name)
        if required_feature is None:
            return
        self.require(required_feature)

    def _remember_canonical_product_routes(self, products: list[Any]) -> None:
        """Retain bounded same-client canonical-to-legacy routes from discovery."""

        for product in products:
            product_id = getattr(product, "product_id", None)
            for declaration in getattr(product, "format_options", None) or []:
                option_id = getattr(declaration, "format_option_id", None)
                if not option_id:
                    continue
                refs = getattr(declaration, "legacy_format_refs", ())
                if not refs:
                    continue
                route = [ref.model_dump(mode="json") for ref in refs]
                publisher = getattr(declaration, "publisher_domain", None)
                self._canonical_legacy_routes[(product_id, publisher, option_id)] = route
                global_key = (None, publisher, option_id)
                existing = self._canonical_legacy_routes.get(global_key)
                if global_key not in self._canonical_legacy_routes:
                    self._canonical_legacy_routes[global_key] = route
                elif existing != route:
                    self._canonical_legacy_routes[global_key] = None

    def _creative_dialect(
        self, request: Any = None, *, legacy_projection_available: bool = False
    ) -> CreativeDialect:
        """Resolve this call's wire dialect from version, capability, and request evidence."""

        request_version = None
        if isinstance(request, dict):
            request_version = request.get("adcp_version")
        elif request is not None:
            request_version = getattr(request, "adcp_version", None)
        version = request_version or self._server_version or self._adcp_version
        if not str(version).startswith("3."):
            return CreativeDialect.LEGACY
        return resolve_creative_dialect(
            version,
            capabilities=self._capabilities,
            request=request,
            legacy_projection_available=legacy_projection_available,
        )

    def _callback_creative_dialect(self, result: Any) -> CreativeDialect:
        """Resolve callbacks from payload evidence, canonical-first when neutral."""

        try:
            return self._creative_dialect(result)
        except CreativeDialectError:
            return CreativeDialect.CANONICAL

    def _legacy_refs_for_option(
        self,
        option: dict[str, Any],
        *,
        product_id: str | None,
        field: str,
    ) -> list[dict[str, Any]]:
        option_id = option.get("format_option_id")
        publisher = option.get("publisher_domain")
        if not isinstance(option_id, str):
            raise CanonicalFormatLegacyResolutionError(f"{field} has no format_option_id")
        route = self._canonical_legacy_routes.get((product_id, publisher, option_id))
        if route is None:
            route = self._canonical_legacy_routes.get((None, publisher, option_id))
        if route is None:
            raise CanonicalFormatLegacyResolutionError(
                f"no discovered legacy route for {field}; rediscover the product or configure "
                "canonical_format_legacy_resolver"
            )
        return [dict(ref) for ref in route]

    def _legacy_refs_for_declaration(
        self,
        declaration: dict[str, Any],
        *,
        product_id: str | None,
        field: str,
    ) -> list[dict[str, Any]]:
        option_id = declaration.get("format_option_id")
        if isinstance(option_id, str):
            try:
                return self._legacy_refs_for_option(
                    declaration,
                    product_id=product_id,
                    field=field,
                )
            except CanonicalFormatLegacyResolutionError:
                pass
        canonical = Format.model_validate(declaration)
        return [
            ref.model_dump(mode="json")
            for ref in resolve_legacy_format_refs(
                canonical,
                resolver=self.canonical_format_legacy_resolver,
                product_id=product_id,
                field=field,
            )
        ]

    def _downgrade_package(self, package: dict[str, Any], *, field: str) -> None:
        product_id = package.get("product_id")
        product_id = product_id if isinstance(product_id, str) else None
        refs = package.pop("format_option_refs", None)
        if refs:
            legacy: list[dict[str, Any]] = []
            for index, option in enumerate(refs):
                legacy.extend(
                    self._legacy_refs_for_option(
                        option,
                        product_id=product_id,
                        field=f"{field}.format_option_refs[{index}]",
                    )
                )
            package["format_ids"] = legacy
        elif package.get("format_kind") is not None:
            declaration = {
                "format_kind": package.pop("format_kind"),
                "params": package.pop("params", None) or {},
            }
            package["format_ids"] = self._legacy_refs_for_declaration(
                declaration,
                product_id=product_id,
                field=f"{field}.format_kind",
            )
        else:
            package.pop("params", None)

        for index, creative in enumerate(package.get("creatives") or []):
            self._downgrade_creative(
                creative,
                field=f"{field}.creatives[{index}]",
                product_id=product_id,
            )

    def _downgrade_creative(
        self,
        creative: dict[str, Any],
        *,
        field: str,
        product_id: str | None = None,
    ) -> None:
        option = creative.pop("format_option_ref", None)
        if option:
            refs = self._legacy_refs_for_option(
                option, product_id=product_id, field=f"{field}.format_option_ref"
            )
        else:
            declaration = {
                "format_kind": creative.pop("format_kind"),
                "params": creative.pop("params", None) or {},
            }
            refs = self._legacy_refs_for_declaration(
                declaration, product_id=product_id, field=field
            )
        if len(refs) != 1:
            raise CanonicalFormatLegacyResolutionError(
                f"{field} resolves to {len(refs)} legacy formats; a creative requires exactly one"
            )
        creative["format_id"] = refs[0]

    def _prepare_creative_params(self, request: BaseModel) -> dict[str, Any]:
        """Serialize canonical input and deterministically adapt legacy peers."""

        params = request.model_dump(mode="json", exclude_none=True)
        if strip_legacy_creative_identity(params) != params:
            raise ValueError(
                "primary creative methods reject legacy format identity; use the explicit "
                "*_legacy method"
            )
        if self._creative_dialect(request) is CreativeDialect.CANONICAL:
            return params
        for collection in ("packages", "new_packages"):
            for index, package in enumerate(params.get(collection) or []):
                self._downgrade_package(package, field=f"{collection}[{index}]")
        for index, creative in enumerate(params.get("creatives") or []):
            self._downgrade_creative(creative, field=f"creatives[{index}]")
        return params

    def _canonicalize_get_products_result(
        self,
        raw_result: TaskResult[Any],
    ) -> TaskResult[GetProductsResponse]:
        """Parse the wire shape, project products, and enforce the primary boundary."""

        # Canonical responses must be parsed before the legacy compatibility
        # model. The generated legacy ProductFormatDeclaration intentionally
        # lacks canonical ``format_kind`` and ``params`` fields, so parsing a
        # canonical-only response through it first irreversibly discards the
        # declaration before ``project_legacy_product`` can inspect it.
        canonical_result: TaskResult[GetProductsResponse] = self.adapter._parse_response(
            raw_result, GetProductsResponse
        )
        if not raw_result.success or raw_result.data is None:
            return canonical_result
        if canonical_result.success and canonical_result.data is not None:
            direct_products = list(canonical_result.data.products or [])
            self._remember_canonical_product_routes(direct_products)
            metadata = dict(canonical_result.metadata or {})
            metadata["projection"] = {"diagnostics": []}
            canonical_result.metadata = metadata
            return canonical_result

        legacy_result: TaskResult[Any] = self.adapter._parse_response(
            raw_result, LegacyGetProductsResponse
        )
        if not legacy_result.success or legacy_result.data is None:
            return cast(TaskResult[GetProductsResponse], legacy_result)

        body = legacy_result.data.model_dump(mode="json")
        canonical_products: list[Product] = []
        diagnostics: list[dict[str, Any]] = []
        portable_errors = list(body.get("errors") or [])
        for raw_product in body.get("products") or []:
            projected = project_legacy_product(
                raw_product,
                legacy_format_converter=self.legacy_format_converter,
            )
            if projected.product is not None:
                canonical_products.append(projected.product)
            for diagnostic in projected.diagnostics:
                dumped = diagnostic.model_dump()
                diagnostics.append(dumped)
                details = dumped["error"]["details"]
                portable_errors.append(
                    {
                        "code": diagnostic.code,
                        "message": (
                            "Product has no format declaration representable on the "
                            "canonical-only surface"
                            if diagnostic.code == "CANONICAL_PRODUCT_FORMATS_UNAVAILABLE"
                            else "Legacy creative format could not be projected to a "
                            "canonical declaration"
                        ),
                        "field": diagnostic.field,
                        "recovery": "correctable",
                        "source": "sdk",
                        "sdk_id": dumped["sdk_id"],
                        "details": details,
                    }
                )

        body["products"] = canonical_products
        if portable_errors:
            body["errors"] = portable_errors
        try:
            canonical = GetProductsResponse.model_validate(body)
        except ValueError as exc:
            return TaskResult[GetProductsResponse](
                status=TaskStatus.FAILED,
                success=False,
                error=f"Failed to project canonical get_products response: {exc}",
                message=legacy_result.message,
                metadata=legacy_result.metadata,
                debug_info=legacy_result.debug_info,
                idempotency_key=legacy_result.idempotency_key,
                replayed=legacy_result.replayed,
            )

        self._remember_canonical_product_routes(canonical_products)
        metadata = dict(legacy_result.metadata or {})
        metadata["projection"] = {"diagnostics": diagnostics}
        return TaskResult[GetProductsResponse](
            status=legacy_result.status,
            data=canonical,
            message=legacy_result.message,
            success=True,
            error=legacy_result.error,
            metadata=metadata,
            debug_info=legacy_result.debug_info,
            idempotency_key=legacy_result.idempotency_key,
            replayed=legacy_result.replayed,
        )

    def _canonicalize_format_read_result(
        self,
        raw_result: TaskResult[Any],
        *,
        legacy_type: Any,
        canonical_type: Any,
        collection: str,
        require_format: bool,
    ) -> TaskResult[Any]:
        """Upgrade legacy creative rows through the same projector used by discovery."""

        legacy_result = self.adapter._parse_response(raw_result, legacy_type)
        if not legacy_result.success or legacy_result.data is None:
            return legacy_result
        body = legacy_result.data.model_dump(mode="json")
        converted: list[dict[str, Any]] = []
        diagnostics: list[dict[str, Any]] = []
        errors = list(body.get("errors") or [])
        for index, raw_item in enumerate(body.get(collection) or []):
            item = dict(raw_item)
            legacy_id = item.pop("format_id", None)
            if item.get("format_kind") is None and legacy_id is not None:
                projection = project_legacy_format_id(
                    legacy_id,
                    product_id=str(item.get("media_buy_id") or ""),
                    field=f"{collection}[{index}].format_id",
                    legacy_format_converter=self.legacy_format_converter,
                )
                if projection.declaration is not None:
                    item["format_kind"] = projection.declaration.format_kind.value
                elif projection.diagnostic is not None:
                    diagnostic = projection.diagnostic.model_dump()
                    diagnostics.append(diagnostic)
                    errors.append(
                        {
                            "code": "FORMAT_PROJECTION_FAILED",
                            "message": "Legacy creative format could not be projected.",
                            "field": f"{collection}[{index}].format_id",
                            "severity": "warning",
                            "source": "sdk",
                            "details": diagnostic["error"]["details"],
                        }
                    )
            if require_format and item.get("format_kind") is None:
                continue
            converted.append(item)
        body[collection] = converted
        if errors:
            body["errors"] = errors
        try:
            canonical = canonical_type.model_validate(body)
        except ValueError as exc:
            return TaskResult[Any](
                status=TaskStatus.FAILED,
                success=False,
                error=f"Failed to project canonical {collection} response: {exc}",
                message=legacy_result.message,
                metadata=legacy_result.metadata,
            )
        metadata = dict(legacy_result.metadata or {})
        metadata["projection"] = {"diagnostics": diagnostics}
        return TaskResult[Any](
            status=legacy_result.status,
            data=canonical,
            message=legacy_result.message,
            success=True,
            metadata=metadata,
            debug_info=legacy_result.debug_info,
            idempotency_key=legacy_result.idempotency_key,
            replayed=legacy_result.replayed,
        )

    def _canonicalize_lifecycle_result(
        self,
        raw_result: TaskResult[Any],
        *,
        legacy_type: Any,
        canonical_type: Any,
    ) -> TaskResult[Any]:
        """Upgrade legacy package/creative selectors on lifecycle responses."""

        legacy_result = self.adapter._parse_response(raw_result, legacy_type)
        if not legacy_result.success or legacy_result.data is None:
            return legacy_result
        try:
            body = normalize_legacy_creative_request(
                legacy_result.data.model_dump(mode="json"),
                legacy_format_converter=self.legacy_format_converter,
            )
            canonical = TypeAdapter(canonical_type).validate_python(body)
        except (LegacyCreativeProjectionError, ValueError) as exc:
            return TaskResult[Any](
                status=TaskStatus.FAILED,
                success=False,
                error=f"Failed to project canonical lifecycle response: {exc}",
                message=legacy_result.message,
                metadata=legacy_result.metadata,
            )
        return TaskResult[Any](
            status=legacy_result.status,
            data=canonical,
            message=legacy_result.message,
            success=True,
            metadata=legacy_result.metadata,
            debug_info=legacy_result.debug_info,
            idempotency_key=legacy_result.idempotency_key,
            replayed=legacy_result.replayed,
        )

    async def _execute_typed_task(
        self,
        task_type: str,
        request: BaseModel,
        response_type: type[BaseModel] | Any,
    ) -> TaskResult[Any]:
        """Execute and parse one typed AdCP task with activity events."""
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)
        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type=task_type,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )
        method = getattr(self.adapter, task_type)
        raw_result = await method(params)
        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type=task_type,
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )
        return self.adapter._parse_response(raw_result, response_type)

    async def list_products(self, request: ListProductsRequest) -> TaskResult[ListProductsResponse]:
        """List products using the AdCP 3.2 compact discovery lifecycle."""
        return cast(
            TaskResult[ListProductsResponse],
            await self._execute_typed_task("list_products", request, ListProductsResponse),
        )

    async def request_proposals(
        self, request: RequestProposalsRequest
    ) -> TaskResult[RequestProposalsResponse]:
        """Request seller proposals for selected products."""
        return cast(
            TaskResult[RequestProposalsResponse],
            await self._execute_typed_task("request_proposals", request, RequestProposalsResponse),
        )

    async def refine_proposals(
        self, request: RefineProposalsRequest
    ) -> TaskResult[RefineProposalsResponse]:
        """Refine one or more seller proposals."""
        return cast(
            TaskResult[RefineProposalsResponse],
            await self._execute_typed_task("refine_proposals", request, RefineProposalsResponse),
        )

    async def decline_proposals(
        self, request: DeclineProposalsRequest
    ) -> TaskResult[DeclineProposalsResponse]:
        """Decline one or more seller proposals."""
        return cast(
            TaskResult[DeclineProposalsResponse],
            await self._execute_typed_task("decline_proposals", request, DeclineProposalsResponse),
        )

    async def buy_products(self, request: BuyProductsRequest) -> TaskResult[BuyProductsResponse]:
        """Commit a direct product purchase."""
        return cast(
            TaskResult[BuyProductsResponse],
            await self._execute_typed_task("buy_products", request, BuyProductsResponse),
        )

    async def accept_proposal(
        self, request: AcceptProposalRequest
    ) -> TaskResult[AcceptProposalResponse]:
        """Accept a seller proposal and create its media buy."""
        return cast(
            TaskResult[AcceptProposalResponse],
            await self._execute_typed_task("accept_proposal", request, AcceptProposalResponse),
        )

    async def control_media_buy(
        self, request: ControlMediaBuyRequest
    ) -> TaskResult[ControlMediaBuyResponse]:
        """Apply lifecycle controls to an existing media buy."""
        return cast(
            TaskResult[ControlMediaBuyResponse],
            await self._execute_typed_task("control_media_buy", request, ControlMediaBuyResponse),
        )

    async def get_products(
        self,
        request: GetProductsRequest,
        fetch_previews: bool = False,
        preview_output_format: str = "url",
        creative_agent_client: ADCPClient | None = None,
    ) -> TaskResult[GetProductsResponse]:
        """
        Get advertising products.

        Args:
            request: Request parameters
            fetch_previews: If True, generate preview URLs for each product's formats
                (uses batch API for 5-10x performance improvement)
            preview_output_format: "url" for iframe URLs (default), "html" for direct
                embedding (2-3x faster, no iframe overhead)
            creative_agent_client: Client for creative agent (required if
                fetch_previews=True)

        Returns:
            TaskResult containing GetProductsResponse with optional preview URLs in metadata

        Raises:
            ValueError: If fetch_previews=True but creative_agent_client is not provided
        """
        if fetch_previews and not creative_agent_client:
            raise ValueError("creative_agent_client is required when fetch_previews=True")

        self._creative_dialect(request, legacy_projection_available=True)
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="get_products",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.get_products(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="get_products",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        result = self._canonicalize_get_products_result(raw_result)

        if (
            fetch_previews
            and result.success
            and result.data
            and result.data.products
            and creative_agent_client
        ):
            from adcp.utils.preview_cache import add_preview_urls_to_products

            products_with_previews = await add_preview_urls_to_products(
                result.data.products,
                creative_agent_client,
                use_batch=True,
                output_format=preview_output_format,
            )
            result.metadata = result.metadata or {}
            result.metadata["products_with_previews"] = products_with_previews

        return result

    async def get_products_legacy(
        self,
        request: LegacyGetProductsRequest,
    ) -> TaskResult[LegacyGetProductsResponse]:
        """Return the raw AdCP 3.x product wire shape for migration tooling."""

        self._warn_legacy_creative_api("get_products_legacy")
        raw_result = await self.adapter.get_products(request.model_dump(mode="json"))
        return self.adapter._parse_response(raw_result, LegacyGetProductsResponse)

    @staticmethod
    def _warn_legacy_creative_api(method: str) -> None:
        warnings.warn(
            f"{method} exposes legacy named-format identity and will be removed with AdCP 4.0",
            DeprecationWarning,
            stacklevel=3,
        )

    async def list_creative_formats_legacy(
        self,
        request: ListCreativeFormatsRequest,
        fetch_previews: bool = False,
        preview_output_format: str = "url",
    ) -> TaskResult[ListCreativeFormatsResponse]:
        """
        List supported creative formats.

        Args:
            request: Request parameters
            fetch_previews: If True, generate preview URLs for each format using
                sample manifests (uses batch API for 5-10x performance improvement)
            preview_output_format: "url" for iframe URLs (default), "html" for direct
                embedding (2-3x faster, no iframe overhead)

        Returns:
            TaskResult containing ListCreativeFormatsResponse with optional preview URLs in metadata
        """
        self._warn_legacy_creative_api("list_creative_formats_legacy")
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="list_creative_formats",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.list_creative_formats(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="list_creative_formats",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        result: TaskResult[ListCreativeFormatsResponse] = self.adapter._parse_response(
            raw_result, ListCreativeFormatsResponse
        )

        if fetch_previews and result.success and result.data:
            from adcp.utils.preview_cache import add_preview_urls_to_formats

            formats_with_previews = await add_preview_urls_to_formats(
                result.data.formats,
                self,
                use_batch=True,
                output_format=preview_output_format,
            )
            result.metadata = result.metadata or {}
            result.metadata["formats_with_previews"] = formats_with_previews

        return result

    async def create_media_buy_legacy(
        self, request: LegacyCreateMediaBuyRequest
    ) -> TaskResult[LegacyCreateMediaBuyResponse]:
        """Execute create_media_buy without the canonical application boundary."""

        self._warn_legacy_creative_api("create_media_buy_legacy")
        raw = await self.adapter.create_media_buy(
            request.model_dump(mode="json", exclude_none=True)
        )
        return self.adapter._parse_response(raw, LegacyCreateMediaBuyResponse)

    async def update_media_buy_legacy(
        self, request: LegacyUpdateMediaBuyRequest
    ) -> TaskResult[LegacyUpdateMediaBuyResponse]:
        """Execute update_media_buy without the canonical application boundary."""

        self._warn_legacy_creative_api("update_media_buy_legacy")
        raw = await self.adapter.update_media_buy(
            request.model_dump(mode="json", exclude_none=True)
        )
        return self.adapter._parse_response(raw, LegacyUpdateMediaBuyResponse)

    async def sync_creatives_legacy(
        self, request: LegacySyncCreativesRequest
    ) -> TaskResult[LegacySyncCreativesResponse]:
        """Execute sync_creatives without the canonical application boundary."""

        self._warn_legacy_creative_api("sync_creatives_legacy")
        raw = await self.adapter.sync_creatives(request.model_dump(mode="json", exclude_none=True))
        return self.adapter._parse_response(raw, LegacySyncCreativesResponse)

    async def list_creatives_legacy(
        self, request: LegacyListCreativesRequest
    ) -> TaskResult[LegacyListCreativesResponse]:
        """Return raw creative rows carrying legacy format identity."""

        self._warn_legacy_creative_api("list_creatives_legacy")
        raw = await self.adapter.list_creatives(request.model_dump(mode="json", exclude_none=True))
        return self.adapter._parse_response(raw, LegacyListCreativesResponse)

    async def get_media_buys_legacy(
        self, request: GetMediaBuysRequest
    ) -> TaskResult[LegacyGetMediaBuysResponse]:
        """Return raw media-buy rows carrying legacy format identity."""

        self._warn_legacy_creative_api("get_media_buys_legacy")
        raw = await self.adapter.get_media_buys(request.model_dump(mode="json", exclude_none=True))
        return self.adapter._parse_response(raw, LegacyGetMediaBuysResponse)

    async def get_media_buy_delivery_legacy(
        self, request: GetMediaBuyDeliveryRequest
    ) -> TaskResult[LegacyGetMediaBuyDeliveryResponse]:
        """Return raw media-buy delivery carrying legacy format identity."""

        self._warn_legacy_creative_api("get_media_buy_delivery_legacy")
        raw = await self.adapter.get_media_buy_delivery(
            request.model_dump(mode="json", exclude_none=True)
        )
        return self.adapter._parse_response(raw, LegacyGetMediaBuyDeliveryResponse)

    async def get_creative_delivery_legacy(
        self, request: GetCreativeDeliveryRequest
    ) -> TaskResult[LegacyGetCreativeDeliveryResponse]:
        """Return raw creative delivery carrying legacy format identity."""

        self._warn_legacy_creative_api("get_creative_delivery_legacy")
        raw = await self.adapter.get_creative_delivery(
            request.model_dump(mode="json", exclude_none=True)
        )
        return self.adapter._parse_response(raw, LegacyGetCreativeDeliveryResponse)

    async def preview_creative_legacy(
        self,
        request: LegacyPreviewCreativeRequest,
    ) -> TaskResult[LegacyPreviewCreativeResponse]:
        """
        Generate preview of a creative manifest.

        Args:
            request: Request parameters

        Returns:
            TaskResult containing PreviewCreativeResponse with preview URLs
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="preview_creative",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.preview_creative(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="preview_creative",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        self._warn_legacy_creative_api("preview_creative_legacy")
        return self.adapter._parse_response(raw_result, LegacyPreviewCreativeResponse)

    async def sync_creatives(
        self,
        request: SyncCreativesRequest,
    ) -> TaskResult[SyncCreativesResponse]:
        """
        Sync Creatives.

        Args:
            request: Request parameters

        Returns:
            TaskResult containing SyncCreativesResponse
        """
        dialect = self._creative_dialect(request, legacy_projection_available=True)
        operation_id = create_operation_id()
        params = self._prepare_creative_params(request)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="sync_creatives",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.sync_creatives(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="sync_creatives",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        if dialect is CreativeDialect.LEGACY:
            return cast(
                TaskResult[SyncCreativesResponse],
                self._canonicalize_lifecycle_result(
                    raw_result,
                    legacy_type=LegacySyncCreativesResponse,
                    canonical_type=SyncCreativesResponse,
                ),
            )
        return self.adapter._parse_response(raw_result, SyncCreativesResponse)

    async def list_creatives(
        self,
        request: ListCreativesRequest,
    ) -> TaskResult[ListCreativesResponse]:
        """
        List Creatives.

        Args:
            request: Request parameters

        Returns:
            TaskResult containing ListCreativesResponse
        """
        dialect = self._creative_dialect(request, legacy_projection_available=True)
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="list_creatives",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.list_creatives(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="list_creatives",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        if dialect is CreativeDialect.CANONICAL:
            return self.adapter._parse_response(raw_result, ListCreativesResponse)
        return cast(
            TaskResult[ListCreativesResponse],
            self._canonicalize_format_read_result(
                raw_result,
                legacy_type=LegacyListCreativesResponse,
                canonical_type=ListCreativesResponse,
                collection="creatives",
                require_format=True,
            ),
        )

    async def get_media_buy_delivery(
        self,
        request: GetMediaBuyDeliveryRequest,
    ) -> TaskResult[GetMediaBuyDeliveryResponse]:
        """
        Get Media Buy Delivery.

        Args:
            request: Request parameters

        Returns:
            TaskResult containing GetMediaBuyDeliveryResponse
        """
        dialect = self._creative_dialect(request, legacy_projection_available=True)
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="get_media_buy_delivery",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.get_media_buy_delivery(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="get_media_buy_delivery",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        if dialect is CreativeDialect.LEGACY:
            return cast(
                TaskResult[GetMediaBuyDeliveryResponse],
                self._canonicalize_lifecycle_result(
                    raw_result,
                    legacy_type=LegacyGetMediaBuyDeliveryResponse,
                    canonical_type=GetMediaBuyDeliveryResponse,
                ),
            )
        return self.adapter._parse_response(raw_result, GetMediaBuyDeliveryResponse)

    async def get_media_buys(
        self,
        request: GetMediaBuysRequest,
    ) -> TaskResult[GetMediaBuysResponse]:
        """
        Get Media Buys.

        Args:
            request: Request parameters

        Returns:
            TaskResult containing GetMediaBuysResponse
        """
        dialect = self._creative_dialect(request, legacy_projection_available=True)
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)
        if params.get("include_webhook_activity") is False:
            params.pop("include_webhook_activity")
        if params.get("webhook_activity_limit") == 50:
            params.pop("webhook_activity_limit")

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="get_media_buys",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.get_media_buys(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="get_media_buys",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        if dialect is CreativeDialect.LEGACY:
            return cast(
                TaskResult[GetMediaBuysResponse],
                self._canonicalize_lifecycle_result(
                    raw_result,
                    legacy_type=LegacyGetMediaBuysResponse,
                    canonical_type=GetMediaBuysResponse,
                ),
            )
        return self.adapter._parse_response(raw_result, GetMediaBuysResponse)

    async def get_signals(
        self,
        request: GetSignalsRequest,
    ) -> TaskResult[GetSignalsResponse]:
        """
        Get Signals.

        Args:
            request: Request parameters

        Returns:
            TaskResult containing GetSignalsResponse
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="get_signals",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.get_signals(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="get_signals",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, GetSignalsResponse)

    async def activate_signal(
        self,
        request: ActivateSignalRequest,
    ) -> TaskResult[ActivateSignalResponse]:
        """
        Activate Signal.

        Args:
            request: Request parameters

        Returns:
            TaskResult containing ActivateSignalResponse
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="activate_signal",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.activate_signal(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="activate_signal",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, ActivateSignalResponse)

    async def provide_performance_feedback(
        self,
        request: ProvidePerformanceFeedbackRequest,
    ) -> TaskResult[ProvidePerformanceFeedbackResponse]:
        """
        Provide Performance Feedback.

        Args:
            request: Request parameters

        Returns:
            TaskResult containing ProvidePerformanceFeedbackResponse
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="provide_performance_feedback",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.provide_performance_feedback(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="provide_performance_feedback",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, ProvidePerformanceFeedbackResponse)

    async def create_media_buy(
        self,
        request: CreateMediaBuyRequest,
    ) -> TaskResult[CreateMediaBuyResponse]:
        """
        Create a new media buy reservation.

        Requests the agent to reserve inventory for a campaign. The agent returns a
        media_buy_id that tracks this reservation and can be used for updates.

        Args:
            request: Media buy creation parameters including:
                - brand: Brand reference; resolved from brand.json or the registry at execution
                - packages: List of package requests specifying desired inventory
                - publisher_properties: Target properties for ad placement
                - budget: Optional budget constraints
                - start_date/end_date: Campaign flight dates

        Returns:
            TaskResult containing CreateMediaBuyResponse with:
                - media_buy_id: Unique identifier for this reservation
                - status: Current state of the media buy
                - packages: Confirmed package details
                - Additional platform-specific metadata

        Example:
            >>> from adcp import ADCPClient, CreateMediaBuyRequest, BrandReference
            >>> client = ADCPClient(agent_config)
            >>> request = CreateMediaBuyRequest(
            ...     brand=BrandReference(domain="acme.com"),
            ...     packages=[package_request],
            ...     publisher_properties=properties,
            ... )
            >>> result = await client.create_media_buy(request)
            >>> if result.success:
            ...     media_buy_id = result.data.media_buy_id
        """
        dialect = self._creative_dialect(request, legacy_projection_available=True)
        operation_id = create_operation_id()
        params = self._prepare_creative_params(request)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="create_media_buy",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.create_media_buy(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="create_media_buy",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        if dialect is CreativeDialect.LEGACY:
            return cast(
                TaskResult[CreateMediaBuyResponse],
                self._canonicalize_lifecycle_result(
                    raw_result,
                    legacy_type=LegacyCreateMediaBuyResponse,
                    canonical_type=CreateMediaBuyResponse,
                ),
            )
        return self.adapter._parse_response(raw_result, CreateMediaBuyResponse)

    async def update_media_buy(
        self,
        request: UpdateMediaBuyRequest,
    ) -> TaskResult[UpdateMediaBuyResponse]:
        """
        Update an existing media buy reservation.

        Modifies a previously created media buy by updating packages or publisher
        properties. The update operation uses discriminated unions to specify what
        to change - either package details or targeting properties.

        Args:
            request: Media buy update parameters including:
                - media_buy_id: Identifier from create_media_buy response
                - updates: Discriminated union specifying update type:
                    * UpdateMediaBuyPackagesRequest: Modify package selections
                    * UpdateMediaBuyPropertiesRequest: Change targeting properties

        Returns:
            TaskResult containing UpdateMediaBuyResponse with:
                - media_buy_id: The updated media buy identifier
                - status: Updated state of the media buy
                - packages: Updated package configurations
                - Additional platform-specific metadata

        Example:
            >>> from adcp import ADCPClient, UpdateMediaBuyPackagesRequest
            >>> client = ADCPClient(agent_config)
            >>> request = UpdateMediaBuyPackagesRequest(
            ...     media_buy_id="mb_123",
            ...     packages=[updated_package]
            ... )
            >>> result = await client.update_media_buy(request)
            >>> if result.success:
            ...     updated_packages = result.data.packages
        """
        dialect = self._creative_dialect(request, legacy_projection_available=True)
        operation_id = create_operation_id()
        params = self._prepare_creative_params(request)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="update_media_buy",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.update_media_buy(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="update_media_buy",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        if dialect is CreativeDialect.LEGACY:
            return cast(
                TaskResult[UpdateMediaBuyResponse],
                self._canonicalize_lifecycle_result(
                    raw_result,
                    legacy_type=LegacyUpdateMediaBuyResponse,
                    canonical_type=UpdateMediaBuyResponse,
                ),
            )
        return self.adapter._parse_response(raw_result, UpdateMediaBuyResponse)

    async def build_creative_legacy(
        self,
        request: LegacyBuildCreativeRequest,
    ) -> TaskResult[LegacyBuildCreativeResponse]:
        """
        Generate production-ready creative assets.

        Requests the creative agent to build final deliverable assets in the target
        format (e.g., VAST, DAAST, HTML5). This is typically called after previewing
        and approving a creative manifest.

        Args:
            request: Creative build parameters including:
                - manifest: Creative manifest with brand info and content
                - target_format_id: Desired output format identifier
                - inputs: Optional user-provided inputs for template variables
                - deployment: Platform or agent deployment configuration

        Returns:
            TaskResult containing BuildCreativeResponse with:
                - assets: Production-ready creative files (URLs or inline content)
                - format_id: The generated format identifier
                - manifest: The creative manifest used for generation
                - metadata: Additional platform-specific details

        Example:
            >>> from adcp import ADCPClient, LegacyBuildCreativeRequest
            >>> client = ADCPClient(agent_config)
            >>> request = LegacyBuildCreativeRequest(
            ...     manifest=creative_manifest,
            ...     target_format_id="vast_2.0",
            ...     inputs={"duration": 30}
            ... )
            >>> result = await client.build_creative_legacy(request)
            >>> if result.success:
            ...     vast_url = result.data.assets[0].url
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="build_creative",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.build_creative(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="build_creative",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        self._warn_legacy_creative_api("build_creative_legacy")
        return self.adapter._parse_response(raw_result, LegacyBuildCreativeResponse)

    async def list_accounts(
        self,
        request: ListAccountsRequest,
    ) -> TaskResult[ListAccountsResponse]:
        """
        List Accounts.

        Args:
            request: Request parameters

        Returns:
            TaskResult containing ListAccountsResponse
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="list_accounts",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.list_accounts(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="list_accounts",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, ListAccountsResponse)

    async def sync_accounts(
        self,
        request: SyncAccountsRequest,
    ) -> TaskResult[SyncAccountsResponse]:
        """
        Sync Accounts.

        Args:
            request: Request parameters

        Returns:
            TaskResult containing SyncAccountsResponse
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="sync_accounts",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.sync_accounts(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="sync_accounts",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, SyncAccountsResponse)

    async def get_account_financials(
        self,
        request: GetAccountFinancialsRequest,
    ) -> TaskResult[GetAccountFinancialsResponse]:
        """
        Get Account Financials.

        Args:
            request: Request parameters

        Returns:
            TaskResult containing GetAccountFinancialsResponse
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="get_account_financials",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.get_account_financials(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="get_account_financials",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, GetAccountFinancialsResponse)

    async def report_usage(
        self,
        request: ReportUsageRequest,
    ) -> TaskResult[ReportUsageResponse]:
        """
        Report Usage.

        Args:
            request: Request parameters

        Returns:
            TaskResult containing ReportUsageResponse
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="report_usage",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.report_usage(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="report_usage",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, ReportUsageResponse)

    async def log_event(
        self,
        request: LogEventRequest,
    ) -> TaskResult[LogEventResponse]:
        """
        Log Event.

        Args:
            request: Request parameters

        Returns:
            TaskResult containing LogEventResponse
        """
        self._validate_task_features("log_event")
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="log_event",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.log_event(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="log_event",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, LogEventResponse)

    async def sync_event_sources(
        self,
        request: SyncEventSourcesRequest,
    ) -> TaskResult[SyncEventSourcesResponse]:
        """
        Sync Event Sources.

        Args:
            request: Request parameters

        Returns:
            TaskResult containing SyncEventSourcesResponse
        """
        self._validate_task_features("sync_event_sources")
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="sync_event_sources",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.sync_event_sources(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="sync_event_sources",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, SyncEventSourcesResponse)

    async def sync_audiences(
        self,
        request: SyncAudiencesRequest,
    ) -> TaskResult[SyncAudiencesResponse]:
        """
        Sync Audiences.

        Args:
            request: Request parameters

        Returns:
            TaskResult containing SyncAudiencesResponse
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="sync_audiences",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.sync_audiences(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="sync_audiences",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, SyncAudiencesResponse)

    async def sync_catalogs(
        self,
        request: SyncCatalogsRequest,
    ) -> TaskResult[SyncCatalogsResponse]:
        """
        Sync Catalogs.

        Args:
            request: Request parameters

        Returns:
            TaskResult containing SyncCatalogsResponse
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="sync_catalogs",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.sync_catalogs(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="sync_catalogs",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, SyncCatalogsResponse)

    async def get_creative_delivery(
        self,
        request: GetCreativeDeliveryRequest,
    ) -> TaskResult[GetCreativeDeliveryResponse]:
        """
        Get Creative Delivery.

        Args:
            request: Request parameters

        Returns:
            TaskResult containing GetCreativeDeliveryResponse
        """
        self._creative_dialect(request, legacy_projection_available=True)
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="get_creative_delivery",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.get_creative_delivery(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="get_creative_delivery",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        if (
            self._creative_dialect(request, legacy_projection_available=True)
            is CreativeDialect.CANONICAL
        ):
            return self.adapter._parse_response(raw_result, GetCreativeDeliveryResponse)
        return cast(
            TaskResult[GetCreativeDeliveryResponse],
            self._canonicalize_format_read_result(
                raw_result,
                legacy_type=LegacyGetCreativeDeliveryResponse,
                canonical_type=GetCreativeDeliveryResponse,
                collection="creatives",
                require_format=False,
            ),
        )

    async def list_transformers(
        self,
        request: ListTransformersRequest,
    ) -> TaskResult[ListTransformersResponse]:
        """
        List Creative Transformers.

        Args:
            request: Request parameters

        Returns:
            TaskResult containing ListTransformersResponse
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="list_transformers",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.list_transformers(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="list_transformers",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, ListTransformersResponse)

    # ========================================================================
    # V3 Protocol Methods - Protocol Discovery
    # ========================================================================

    async def get_adcp_capabilities(
        self,
        request: GetAdcpCapabilitiesRequest,
    ) -> TaskResult[GetAdcpCapabilitiesResponse]:
        """
        Get AdCP capabilities from the agent.

        Queries the agent's supported AdCP features, protocol versions, and
        domain-specific capabilities (media_buy, signals, sponsored_intelligence).

        Args:
            request: Request parameters including optional protocol filters

        Returns:
            TaskResult containing GetAdcpCapabilitiesResponse with:
                - adcp: Core protocol version information
                - supported_protocols: List of supported domain protocols
                - media_buy: Media buy capabilities (if supported)
                - sponsored_intelligence: SI capabilities (if supported)
                - signals: Signals capabilities (if supported)
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="get_adcp_capabilities",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.get_adcp_capabilities(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="get_adcp_capabilities",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, GetAdcpCapabilitiesResponse)

    async def sync_agent_notification_configs(
        self,
        request: SyncAgentNotificationConfigsRequest,
    ) -> TaskResult[SyncAgentNotificationConfigsResponse]:
        """Replace the caller-scoped agent notification subscriber set."""
        return cast(
            TaskResult[SyncAgentNotificationConfigsResponse],
            await self._execute_typed_task(
                "sync_agent_notification_configs",
                request,
                SyncAgentNotificationConfigsResponse,
            ),
        )

    async def get_task_status(
        self,
        request: GetTaskStatusRequest,
    ) -> TaskResult[GetTaskStatusResponse]:
        """
        Get Task Status.

        Args:
            request: Request parameters

        Returns:
            TaskResult containing GetTaskStatusResponse
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="get_task_status",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.get_task_status(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="get_task_status",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, GetTaskStatusResponse)

    async def list_tasks(
        self,
        request: ListTasksRequest,
    ) -> TaskResult[ListTasksResponse]:
        """
        List Tasks.

        Args:
            request: Request parameters

        Returns:
            TaskResult containing ListTasksResponse
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="list_tasks",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.list_tasks(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="list_tasks",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, ListTasksResponse)

    # ========================================================================
    # V3 Protocol Methods - Content Standards
    # ========================================================================

    async def create_content_standards(
        self,
        request: CreateContentStandardsRequest,
    ) -> TaskResult[CreateContentStandardsResponse]:
        """
        Create a new content standards configuration.

        Defines acceptable content contexts for ad placement using natural
        language policy and optional calibration exemplars.

        Args:
            request: Request parameters including policy and scope

        Returns:
            TaskResult containing CreateContentStandardsResponse with standards_id
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="create_content_standards",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.create_content_standards(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="create_content_standards",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, CreateContentStandardsResponse)

    async def get_content_standards(
        self,
        request: GetContentStandardsRequest,
    ) -> TaskResult[GetContentStandardsResponse]:
        """
        Get a content standards configuration by ID.

        Args:
            request: Request parameters including standards_id

        Returns:
            TaskResult containing GetContentStandardsResponse
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="get_content_standards",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.get_content_standards(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="get_content_standards",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, GetContentStandardsResponse)

    async def list_content_standards(
        self,
        request: ListContentStandardsRequest,
    ) -> TaskResult[ListContentStandardsResponse]:
        """
        List content standards configurations.

        Args:
            request: Request parameters including optional filters

        Returns:
            TaskResult containing ListContentStandardsResponse
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="list_content_standards",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.list_content_standards(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="list_content_standards",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, ListContentStandardsResponse)

    async def update_content_standards(
        self,
        request: UpdateContentStandardsRequest,
    ) -> TaskResult[UpdateContentStandardsResponse]:
        """
        Update a content standards configuration.

        Args:
            request: Request parameters including standards_id and updates

        Returns:
            TaskResult containing UpdateContentStandardsResponse
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="update_content_standards",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.update_content_standards(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="update_content_standards",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, UpdateContentStandardsResponse)

    async def calibrate_content(
        self,
        request: CalibrateContentRequest,
    ) -> TaskResult[CalibrateContentResponse]:
        """
        Calibrate content against standards.

        Evaluates content (artifact or URL) against configured standards to
        determine suitability for ad placement.

        Args:
            request: Request parameters including content to evaluate

        Returns:
            TaskResult containing CalibrateContentResponse with verdict
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="calibrate_content",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.calibrate_content(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="calibrate_content",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, CalibrateContentResponse)

    async def validate_content_delivery(
        self,
        request: ValidateContentDeliveryRequest,
    ) -> TaskResult[ValidateContentDeliveryResponse]:
        """
        Validate content delivery against standards.

        Validates that ad delivery records comply with content standards.

        Args:
            request: Request parameters including delivery records

        Returns:
            TaskResult containing ValidateContentDeliveryResponse
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="validate_content_delivery",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.validate_content_delivery(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="validate_content_delivery",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, ValidateContentDeliveryResponse)

    async def get_media_buy_artifacts(
        self,
        request: GetMediaBuyArtifactsRequest,
    ) -> TaskResult[GetMediaBuyArtifactsResponse]:
        """
        Get artifacts associated with a media buy.

        Retrieves content artifacts where ads were delivered for a media buy.

        Args:
            request: Request parameters including media_buy_id

        Returns:
            TaskResult containing GetMediaBuyArtifactsResponse
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="get_media_buy_artifacts",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.get_media_buy_artifacts(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="get_media_buy_artifacts",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, GetMediaBuyArtifactsResponse)

    # ========================================================================
    # V3 Protocol Methods - Sponsored Intelligence
    # ========================================================================

    async def si_get_offering(
        self,
        request: SiGetOfferingRequest,
    ) -> TaskResult[SiGetOfferingResponse]:
        """
        Get sponsored intelligence offering.

        Retrieves product/service offerings that can be presented in a
        sponsored intelligence session.

        Args:
            request: Request parameters including brand context

        Returns:
            TaskResult containing SiGetOfferingResponse
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="si_get_offering",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.si_get_offering(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="si_get_offering",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, SiGetOfferingResponse)

    async def si_initiate_session(
        self,
        request: SiInitiateSessionRequest,
    ) -> TaskResult[SiInitiateSessionResponse]:
        """
        Initiate a sponsored intelligence session.

        Starts a conversational brand experience session with a user.

        Args:
            request: Request parameters including identity and context

        Returns:
            TaskResult containing SiInitiateSessionResponse with session_id
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="si_initiate_session",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.si_initiate_session(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="si_initiate_session",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, SiInitiateSessionResponse)

    async def si_send_message(
        self,
        request: SiSendMessageRequest,
    ) -> TaskResult[SiSendMessageResponse]:
        """
        Send a message in a sponsored intelligence session.

        Continues the conversation in an active SI session.

        Args:
            request: Request parameters including session_id and message

        Returns:
            TaskResult containing SiSendMessageResponse with brand response
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="si_send_message",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.si_send_message(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="si_send_message",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, SiSendMessageResponse)

    async def si_terminate_session(
        self,
        request: SiTerminateSessionRequest,
    ) -> TaskResult[SiTerminateSessionResponse]:
        """
        Terminate a sponsored intelligence session.

        Ends an active SI session, optionally with follow-up actions.

        Args:
            request: Request parameters including session_id and termination context

        Returns:
            TaskResult containing SiTerminateSessionResponse
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="si_terminate_session",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.si_terminate_session(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="si_terminate_session",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, SiTerminateSessionResponse)

    # ========================================================================
    # V3 Governance Methods
    # ========================================================================

    async def get_creative_features(
        self,
        request: GetCreativeFeaturesRequest,
    ) -> TaskResult[GetCreativeFeaturesResponse]:
        """Evaluate governance features for a creative manifest."""
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="get_creative_features",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.get_creative_features(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="get_creative_features",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, GetCreativeFeaturesResponse)

    async def sync_plans(
        self,
        request: SyncPlansRequest,
    ) -> TaskResult[SyncPlansResponse]:
        """Sync campaign governance plans to the governance agent."""
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="sync_plans",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.sync_plans(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="sync_plans",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, SyncPlansResponse)

    async def check_governance(
        self,
        request: CheckGovernanceRequest,
    ) -> TaskResult[CheckGovernanceResponse]:
        """Check a proposed or committed action against campaign governance."""
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="check_governance",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.check_governance(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="check_governance",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, CheckGovernanceResponse)

    async def report_plan_outcome(
        self,
        request: ReportPlanOutcomeRequest,
    ) -> TaskResult[ReportPlanOutcomeResponse]:
        """Report the outcome of a governed action to the governance agent."""
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="report_plan_outcome",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.report_plan_outcome(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="report_plan_outcome",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, ReportPlanOutcomeResponse)

    async def report_plan_adjustment(
        self,
        request: ReportPlanAdjustmentRequest,
    ) -> TaskResult[ReportPlanAdjustmentResponse]:
        """Report or review an adjustment to a governed plan outcome."""
        return cast(
            TaskResult[ReportPlanAdjustmentResponse],
            await self._execute_typed_task(
                "report_plan_adjustment", request, ReportPlanAdjustmentResponse
            ),
        )

    async def get_plan_audit_logs(
        self,
        request: GetPlanAuditLogsRequest,
    ) -> TaskResult[GetPlanAuditLogsResponse]:
        """Retrieve governance state and audit logs for one or more plans."""
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="get_plan_audit_logs",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.get_plan_audit_logs(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="get_plan_audit_logs",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, GetPlanAuditLogsResponse)

    async def create_property_list(
        self,
        request: CreatePropertyListRequest,
    ) -> TaskResult[CreatePropertyListResponse]:
        """
        Create a property list for governance filtering.

        Property lists define dynamic sets of properties based on filters,
        brand manifests, and feature requirements.

        Args:
            request: Request parameters for creating the property list

        Returns:
            TaskResult containing CreatePropertyListResponse with list_id
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="create_property_list",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.create_property_list(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="create_property_list",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, CreatePropertyListResponse)

    async def get_property_list(
        self,
        request: GetPropertyListRequest,
    ) -> TaskResult[GetPropertyListResponse]:
        """
        Get a property list with optional resolution.

        When resolve=true, returns the list of resolved property identifiers.
        Use this to get the actual properties that match the list's filters.

        Args:
            request: Request parameters including list_id and resolve flag

        Returns:
            TaskResult containing GetPropertyListResponse with identifiers
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="get_property_list",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.get_property_list(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="get_property_list",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, GetPropertyListResponse)

    async def list_property_lists(
        self,
        request: ListPropertyListsRequest,
    ) -> TaskResult[ListPropertyListsResponse]:
        """
        List property lists owned by a principal.

        Retrieves metadata for all property lists, optionally filtered
        by principal or pagination parameters.

        Args:
            request: Request parameters with optional filtering

        Returns:
            TaskResult containing ListPropertyListsResponse
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="list_property_lists",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.list_property_lists(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="list_property_lists",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, ListPropertyListsResponse)

    async def update_property_list(
        self,
        request: UpdatePropertyListRequest,
    ) -> TaskResult[UpdatePropertyListResponse]:
        """
        Update a property list.

        Modifies the filters, brand manifest, or other parameters
        of an existing property list.

        Args:
            request: Request parameters with list_id and updates

        Returns:
            TaskResult containing UpdatePropertyListResponse
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="update_property_list",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.update_property_list(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="update_property_list",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, UpdatePropertyListResponse)

    async def delete_property_list(
        self,
        request: DeletePropertyListRequest,
    ) -> TaskResult[DeletePropertyListResponse]:
        """
        Delete a property list.

        Removes a property list. Any active subscriptions to this list
        will be terminated.

        Args:
            request: Request parameters with list_id

        Returns:
            TaskResult containing DeletePropertyListResponse
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="delete_property_list",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.delete_property_list(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="delete_property_list",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, DeletePropertyListResponse)

    # ========================================================================
    # V3 Protocol Methods - Governance (Collection Lists)
    # ========================================================================

    async def create_collection_list(
        self,
        request: CreateCollectionListRequest,
    ) -> TaskResult[CreateCollectionListResponse]:
        """Create a collection list for governance filtering.

        Collection lists define dynamic sets of collections (properties, segments, etc.)
        that can be referenced by authorization rules and audience scoping.

        Args:
            request: Request parameters for creating the collection list

        Returns:
            TaskResult containing CreateCollectionListResponse with list_id
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="create_collection_list",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.create_collection_list(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="create_collection_list",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, CreateCollectionListResponse)

    async def get_collection_list(
        self,
        request: GetCollectionListRequest,
    ) -> TaskResult[GetCollectionListResponse]:
        """Get a collection list with optional resolution.

        When resolve=true, returns the resolved members of the collection list.

        Args:
            request: Request parameters including list_id and resolve flag

        Returns:
            TaskResult containing GetCollectionListResponse
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="get_collection_list",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.get_collection_list(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="get_collection_list",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, GetCollectionListResponse)

    async def list_collection_lists(
        self,
        request: ListCollectionListsRequest,
    ) -> TaskResult[ListCollectionListsResponse]:
        """List collection lists owned by a principal.

        Args:
            request: Request parameters with optional filtering

        Returns:
            TaskResult containing ListCollectionListsResponse
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="list_collection_lists",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.list_collection_lists(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="list_collection_lists",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, ListCollectionListsResponse)

    async def update_collection_list(
        self,
        request: UpdateCollectionListRequest,
    ) -> TaskResult[UpdateCollectionListResponse]:
        """Update a collection list.

        Args:
            request: Request parameters with list_id and updates

        Returns:
            TaskResult containing UpdateCollectionListResponse
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="update_collection_list",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.update_collection_list(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="update_collection_list",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, UpdateCollectionListResponse)

    async def delete_collection_list(
        self,
        request: DeleteCollectionListRequest,
    ) -> TaskResult[DeleteCollectionListResponse]:
        """Delete a collection list.

        Args:
            request: Request parameters with list_id

        Returns:
            TaskResult containing DeleteCollectionListResponse
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="delete_collection_list",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.delete_collection_list(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="delete_collection_list",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, DeleteCollectionListResponse)

    # ========================================================================
    # V3 Protocol Methods - Governance (Sync Governance)
    # ========================================================================

    async def sync_governance(
        self,
        request: SyncGovernanceRequest,
    ) -> TaskResult[SyncGovernanceResponse]:
        """Sync governance agents attached to an account.

        Attach, detach, or replace the set of governance agents that must be
        consulted for plan approval on an account.

        Args:
            request: Request parameters with account and governance agents

        Returns:
            TaskResult containing SyncGovernanceResponse
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="sync_governance",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.sync_governance(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="sync_governance",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, SyncGovernanceResponse)

    # ========================================================================
    # V3 Protocol Methods - Temporal Matching Protocol (TMP)
    # ========================================================================

    async def context_match(
        self,
        request: ContextMatchRequest,
    ) -> TaskResult[ContextMatchResponse]:
        """Match ad context to buyer packages.

        Evaluates contextual signals for a publisher placement against the
        buyer's active packages and returns matching offers.

        Args:
            request: Context match request with placement, property, and
                optional artifact refs, context signals, and geo data.

        Returns:
            TaskResult containing ContextMatchResponse with offers.
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True, by_alias=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="context_match",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.context_match(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="context_match",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, ContextMatchResponse)

    async def identity_match(
        self,
        request: IdentityMatchRequest,
    ) -> TaskResult[IdentityMatchResponse]:
        """Match user identity for package eligibility.

        Evaluates a user identity token against all active packages for
        frequency capping and personalization.

        Args:
            request: Identity match request with user_token, uid_type,
                and package_ids.

        Returns:
            TaskResult containing IdentityMatchResponse with eligible_package_ids.
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True, by_alias=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="identity_match",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.identity_match(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="identity_match",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, IdentityMatchResponse)

    # ========================================================================
    # V3 Protocol Methods - Brand Rights
    # ========================================================================

    async def get_brand_identity(
        self,
        request: GetBrandIdentityRequest,
    ) -> TaskResult[GetBrandIdentityResponse]:
        """Get brand identity information.

        Retrieves brand identity data including logos, colors, fonts,
        voice synthesis config, and rights availability.

        Args:
            request: Request with brand_id and optional fields filter.

        Returns:
            TaskResult containing GetBrandIdentityResponse.
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="get_brand_identity",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.get_brand_identity(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="get_brand_identity",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, GetBrandIdentityResponse)

    async def get_rights(
        self,
        request: GetRightsRequest,
    ) -> TaskResult[GetRightsResponse]:
        """Get available rights for licensing.

        Searches for rights offerings using natural language query and
        filters by type, uses, countries, and buyer compatibility.

        Args:
            request: Request with query, uses, and optional filters.

        Returns:
            TaskResult containing GetRightsResponse with matched rights.
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="get_rights",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.get_rights(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="get_rights",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, GetRightsResponse)

    async def acquire_rights(
        self,
        request: AcquireRightsRequest,
    ) -> TaskResult[AcquireRightsResponse]:
        """Acquire rights for brand content usage.

        Binding contractual request to license rights for a campaign.
        Returns credentials for generating rights-cleared content.

        Args:
            request: Request with rights_id, pricing_option_id, buyer,
                campaign, and revocation_webhook.

        Returns:
            TaskResult containing AcquireRightsResponse (acquired,
            pending_approval, rejected, or error).
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="acquire_rights",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.acquire_rights(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="acquire_rights",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, AcquireRightsResponse)

    async def update_rights(
        self,
        request: UpdateRightsRequest,
    ) -> TaskResult[UpdateRightsResponse]:
        """Update terms of an existing rights acquisition.

        Modifies a previously acquired rights record — typically to extend
        the ``end_date``, raise the ``impression_cap``, pause/unpause via
        ``paused``, or swap to a compatible ``pricing_option_id``. Partial
        update: pass only the fields you want to change.

        Failure modes (surface as ``TaskResult`` with ``success=False``):

        * Acquisition is expired or revoked — the seller rejects the update
          outright; mint a fresh ``acquire_rights`` instead.
        * ``pricing_option_id`` swap to an incompatible option — rejected;
          the new option's terms must be a strict superset / compatible
          with the original acquisition.
        * No partial-state mutations on rejection: the acquisition remains
          at its prior state when any field fails validation.

        Args:
            request: Request with ``rights_id`` and at least one mutable
                field (``end_date``, ``impression_cap``, ``paused``, or
                ``pricing_option_id``).

        Returns:
            TaskResult containing UpdateRightsResponse (updated or error).
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="update_rights",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.update_rights(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="update_rights",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, UpdateRightsResponse)

    async def validate_input(self, request: Any) -> TaskResult[Any]:
        """Validate creative input against a format declaration."""
        from adcp.types import _generated as gen

        params = request.model_dump(mode="json", exclude_none=True)
        raw_result = await self.adapter.validate_input(params)
        return self.adapter._parse_response(raw_result, gen.ValidateInputResponse)

    async def verify_brand_claim(self, request: Any) -> TaskResult[Any]:
        """Verify a single brand claim."""
        from adcp.types import _generated as gen

        params = request.model_dump(mode="json", exclude_none=True)
        raw_result = await self.adapter.verify_brand_claim(params)
        return self.adapter._parse_response(raw_result, gen.VerifyBrandClaimResponse)

    async def verify_brand_claims(self, request: Any) -> TaskResult[Any]:
        """Verify multiple brand claims."""
        from adcp.types import _generated as gen

        params = request.model_dump(mode="json", exclude_none=True)
        raw_result = await self.adapter.verify_brand_claims(params)
        return self.adapter._parse_response(raw_result, gen.VerifyBrandClaimsResponseBulk)

    # ========================================================================
    # V3 Protocol Methods - Compliance
    # ========================================================================

    async def comply_test_controller(
        self,
        request: ComplyTestControllerRequest,
    ) -> TaskResult[ComplyTestControllerResponse]:
        """Compliance test controller for sandbox testing.

        Enables sellers to simulate state transitions and delivery data
        in a sandbox environment for compliance testing.

        Args:
            request: Request specifying scenario and parameters.

        Returns:
            TaskResult containing ComplyTestControllerResponse.
        """
        operation_id = create_operation_id()
        params = request.model_dump(mode="json", exclude_none=True)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_REQUEST,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="comply_test_controller",
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        raw_result = await self.adapter.comply_test_controller(params)

        self._emit_activity(
            Activity(
                type=ActivityType.PROTOCOL_RESPONSE,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type="comply_test_controller",
                status=raw_result.status,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
        )

        return self.adapter._parse_response(raw_result, ComplyTestControllerResponse)

    async def execute_task(self, task_name: str, request: BaseModel) -> TaskResult[Any]:
        """Execute a standard task through the canonical primary API map."""

        legacy_only_tasks = {
            "build_creative",
            "list_creative_formats",
            "preview_creative",
        }
        if task_name in legacy_only_tasks:
            raise ValueError(
                f"{task_name} is legacy-only; use execute_task_legacy() or the "
                "corresponding *_legacy method"
            )
        serialized = request.model_dump(mode="json", exclude_none=True)
        if strip_legacy_creative_identity(serialized) != serialized:
            raise ValueError(
                f"{task_name} request contains legacy creative identity; generic execute_task "
                "is canonical-only"
            )
        method = getattr(self, task_name, None)
        if method is None or not callable(method) or task_name.endswith("_legacy"):
            raise ValueError(f"Unknown canonical AdCP task: {task_name}")
        return cast(TaskResult[Any], await method(request))

    async def execute_task_legacy(self, task_name: str, request: BaseModel) -> TaskResult[Any]:
        """Execute an explicitly raw creative task for migration tooling."""

        methods: dict[str, Callable[[Any], Any]] = {
            "build_creative": self.build_creative_legacy,
            "create_media_buy": self.create_media_buy_legacy,
            "get_creative_delivery": self.get_creative_delivery_legacy,
            "get_media_buy_delivery": self.get_media_buy_delivery_legacy,
            "get_media_buys": self.get_media_buys_legacy,
            "get_products": self.get_products_legacy,
            "list_creative_formats": self.list_creative_formats_legacy,
            "list_creatives": self.list_creatives_legacy,
            "preview_creative": self.preview_creative_legacy,
            "sync_creatives": self.sync_creatives_legacy,
            "update_media_buy": self.update_media_buy_legacy,
        }
        method = methods.get(task_name)
        if method is None:
            raise ValueError(
                f"No explicit legacy task adapter for {task_name!r}; "
                "use the raw protocol adapter for conformance tooling"
            )
        return cast(TaskResult[Any], await method(request))

    async def list_tools(self) -> list[str]:
        """
        List available tools from the agent.

        Returns:
            List of tool names
        """
        return await self.adapter.list_tools()

    async def get_info(self) -> dict[str, Any]:
        """
        Get agent information including AdCP extension metadata.

        Returns agent card information including:
        - Agent name, description, version
        - Protocol type (mcp or a2a)
        - AdCP version (from extensions.adcp.adcp_version)
        - Supported protocols (from extensions.adcp.protocols_supported)
        - Available tools/skills

        Returns:
            Dictionary with agent metadata
        """
        return await self.adapter.get_agent_info()

    async def close(self) -> None:
        """Close the adapter and clean up resources."""
        if hasattr(self.adapter, "close"):
            logger.debug(f"Closing adapter for agent {self.agent_config.id}")
            await self.adapter.close()

    async def close_mcp_session(self, session_id: str | None = None) -> None:
        """Explicitly terminate a stateful MCP Streamable HTTP session.

        This sends ``DELETE`` to the configured MCP endpoint with the
        ``Mcp-Session-Id`` header. When ``session_id`` is omitted, the
        SDK-managed current session is closed. It is only valid for MCP
        agents using ``mcp_transport="streamable_http"``.
        """
        if not isinstance(self.adapter, MCPAdapter):
            raise TypeError(
                "close_mcp_session is only supported for MCP clients; "
                f"got {self.agent_config.protocol}"
            )
        await self.adapter.close_mcp_session(session_id)

    async def __aenter__(self) -> ADCPClient:
        """Async context manager entry."""
        return self

    async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
        """Async context manager exit."""
        await self.close()

    def _verify_webhook_signature(
        self,
        payload: dict[str, Any],
        signature: str,
        timestamp: str,
        raw_body: bytes | str | None = None,
    ) -> bool:
        """
        Verify HMAC-SHA256 signature of webhook payload.

        The verification algorithm matches get_adcp_signed_headers_for_webhook:
        1. Constructs message as "{timestamp}.{raw_http_body_bytes}"
        2. HMAC-SHA256 signs with the shared secret
        3. Compares against the provided signature (with "sha256=" prefix stripped)
           using constant-time comparison.

        Per AdCP spec (adcontextprotocol/adcp#2478): verifiers MUST use the raw
        HTTP body bytes captured before any JSON parse; they SHOULD NOT
        re-serialize a parsed payload to reconstruct the signed bytes, because
        re-serialization silently fails against signers whose output differs in
        separator choice, key order, unicode escapes, or number formatting —
        masking signer bugs the verifier should surface. Callers that genuinely
        cannot capture raw bytes MUST fail closed.

        This implementation therefore rejects verification attempts that don't
        supply ``raw_body``. Capture it from your framework's pre-parse hook
        (FastAPI ``Request.body()``, Flask ``request.get_data(cache=True)``,
        aiohttp ``Request.read()``, Express ``express.raw()``).

        Args:
            payload: Parsed webhook payload dict (not used for signing; kept
                for signature parity with callers, but verification derives
                solely from ``raw_body``).
            signature: Signature to verify (with or without "sha256=" prefix)
            timestamp: Unix timestamp in seconds from X-AdCP-Timestamp header
            raw_body: Raw HTTP request body bytes as received on the wire,
                captured before any JSON parse. Required.

        Returns:
            True if signature is valid, False otherwise (including when
            ``raw_body`` is missing — fails closed per spec).
        """
        if not self.webhook_secret:
            logger.error("Webhook signature verification failed: no webhook_secret configured")
            return False

        # Fail closed per adcontextprotocol/adcp#2478: verifiers that cannot
        # capture raw bytes MUST reject, surfacing the infrastructure gap
        # rather than silently reconstructing a signed body that may diverge
        # from the bytes the signer actually hashed.
        if raw_body is None:
            logger.error(
                "Webhook signature verification failed: raw_body is required. "
                "Capture the raw HTTP body pre-parse and pass it to "
                "handle_webhook(raw_body=...). See "
                "https://adcontextprotocol.org/docs/building/implementation/security"
                "#legacy-hmac-sha256-fallback-deprecated-removed-in-40"
            )
            return False

        # Reject stale or future timestamps to prevent replay attacks
        try:
            ts = int(timestamp)
        except (ValueError, TypeError):
            return False
        now = int(time.time())
        if abs(now - ts) > self.webhook_timestamp_tolerance:
            return False

        # Strip "sha256=" prefix if present
        if signature.startswith("sha256="):
            signature = signature[7:]

        payload_str = raw_body.decode("utf-8") if isinstance(raw_body, bytes) else raw_body

        # Construct signed message: timestamp.payload
        signed_message = f"{timestamp}.{payload_str}"

        # Generate expected signature
        expected_signature = hmac.new(
            self.webhook_secret.encode("utf-8"), signed_message.encode("utf-8"), hashlib.sha256
        ).hexdigest()

        return hmac.compare_digest(signature, expected_signature)

    def _parse_webhook_result(
        self,
        task_id: str,
        task_type: str,
        operation_id: str,
        status: GeneratedTaskStatus,
        result: Any,
        timestamp: datetime | str,
        message: str | None,
        context_id: str | None,
        *,
        preserve_legacy_identity: bool = False,
    ) -> TaskResult[AdcpAsyncResponseData]:
        """
        Parse webhook data into typed TaskResult based on task_type.

        Args:
            task_id: Unique identifier for this task
            task_type: Task type from application routing (e.g., "get_products")
            operation_id: Operation identifier from application routing
            status: Current task status
            result: Task-specific payload (AdCP response data)
            timestamp: ISO 8601 timestamp when webhook was generated
            message: Human-readable summary of task state
            context_id: Session/conversation identifier

        Returns:
            TaskResult with task-specific typed response data

        Note:
            This method works with both MCP and A2A protocols by accepting
            protocol-agnostic parameters rather than protocol-specific objects.
        """
        from adcp.utils.response_parser import parse_json_or_text

        # Map task types to their response types (using string literals, not enum)
        # Note: Some response types are Union types (e.g., ActivateSignalResponse = Success | Error)
        response_type_map: dict[str, type[BaseModel] | Any] = {
            # Core operations
            "get_products": GetProductsResponse,
            "list_products": ListProductsResponse,
            "request_proposals": RequestProposalsResponse,
            "refine_proposals": RefineProposalsResponse,
            "decline_proposals": DeclineProposalsResponse,
            "buy_products": BuyProductsResponse,
            "accept_proposal": AcceptProposalResponse,
            "control_media_buy": ControlMediaBuyResponse,
            "list_creative_formats": ListCreativeFormatsResponse,
            "sync_creatives": SyncCreativesResponse,
            "list_creatives": ListCreativesResponse,
            "build_creative": LegacyBuildCreativeResponse,
            "preview_creative": LegacyPreviewCreativeResponse,
            "create_media_buy": CreateMediaBuyResponse,
            "update_media_buy": UpdateMediaBuyResponse,
            "get_media_buy_delivery": GetMediaBuyDeliveryResponse,
            "get_media_buys": GetMediaBuysResponse,
            "get_signals": GetSignalsResponse,
            "activate_signal": ActivateSignalResponse,
            "provide_performance_feedback": ProvidePerformanceFeedbackResponse,
            "report_usage": ReportUsageResponse,
            "get_account_financials": GetAccountFinancialsResponse,
            "list_accounts": ListAccountsResponse,
            "sync_accounts": SyncAccountsResponse,
            "log_event": LogEventResponse,
            "sync_event_sources": SyncEventSourcesResponse,
            "sync_audiences": SyncAudiencesResponse,
            "sync_catalogs": SyncCatalogsResponse,
            "get_creative_delivery": GetCreativeDeliveryResponse,
            # V3 Protocol Discovery
            "get_adcp_capabilities": GetAdcpCapabilitiesResponse,
            "sync_agent_notification_configs": SyncAgentNotificationConfigsResponse,
            # V3 Content Standards
            "create_content_standards": CreateContentStandardsResponse,
            "get_content_standards": GetContentStandardsResponse,
            "list_content_standards": ListContentStandardsResponse,
            "update_content_standards": UpdateContentStandardsResponse,
            "calibrate_content": CalibrateContentResponse,
            "validate_content_delivery": ValidateContentDeliveryResponse,
            "get_media_buy_artifacts": GetMediaBuyArtifactsResponse,
            # V3 Sponsored Intelligence
            "si_get_offering": SiGetOfferingResponse,
            "si_initiate_session": SiInitiateSessionResponse,
            "si_send_message": SiSendMessageResponse,
            "si_terminate_session": SiTerminateSessionResponse,
            # V3 Governance
            "get_creative_features": GetCreativeFeaturesResponse,
            "sync_plans": SyncPlansResponse,
            "check_governance": CheckGovernanceResponse,
            "report_plan_outcome": ReportPlanOutcomeResponse,
            "report_plan_adjustment": ReportPlanAdjustmentResponse,
            "get_plan_audit_logs": GetPlanAuditLogsResponse,
            "create_property_list": CreatePropertyListResponse,
            "get_property_list": GetPropertyListResponse,
            "list_property_lists": ListPropertyListsResponse,
            "update_property_list": UpdatePropertyListResponse,
            "delete_property_list": DeletePropertyListResponse,
            # TMP
            "context_match": ContextMatchResponse,
            "identity_match": IdentityMatchResponse,
            # Brand Rights
            "get_brand_identity": GetBrandIdentityResponse,
            "get_rights": GetRightsResponse,
            "acquire_rights": AcquireRightsResponse,
            "update_rights": UpdateRightsResponse,
            # Compliance
            "comply_test_controller": ComplyTestControllerResponse,
        }

        if preserve_legacy_identity:
            response_type_map.update(
                {
                    "get_products": LegacyGetProductsResponse,
                    "list_creative_formats": ListCreativeFormatsResponse,
                    "sync_creatives": LegacySyncCreativesResponse,
                    "list_creatives": LegacyListCreativesResponse,
                    "build_creative": LegacyBuildCreativeResponse,
                    "preview_creative": LegacyPreviewCreativeResponse,
                    "create_media_buy": LegacyCreateMediaBuyResponse,
                    "update_media_buy": LegacyUpdateMediaBuyResponse,
                    "get_media_buy_delivery": LegacyGetMediaBuyDeliveryResponse,
                    "get_media_buys": LegacyGetMediaBuysResponse,
                    "get_creative_delivery": LegacyGetCreativeDeliveryResponse,
                }
            )

        # Handle completed tasks with result parsing
        if status == GeneratedTaskStatus.completed and result is not None:
            if task_type == "get_products" and not preserve_legacy_identity:
                projected = self._canonicalize_get_products_result(
                    TaskResult[Any](
                        status=TaskStatus.COMPLETED,
                        data=result,
                        success=True,
                        message=message,
                        metadata={
                            "task_id": task_id,
                            "operation_id": operation_id,
                            "timestamp": timestamp,
                            "message": message,
                        },
                    )
                )
                return cast(TaskResult[AdcpAsyncResponseData], projected)
            legacy_lifecycle_types = {
                "create_media_buy": (LegacyCreateMediaBuyResponse, CreateMediaBuyResponse),
                "update_media_buy": (LegacyUpdateMediaBuyResponse, UpdateMediaBuyResponse),
                "sync_creatives": (LegacySyncCreativesResponse, SyncCreativesResponse),
                "get_media_buys": (LegacyGetMediaBuysResponse, GetMediaBuysResponse),
                "get_media_buy_delivery": (
                    LegacyGetMediaBuyDeliveryResponse,
                    GetMediaBuyDeliveryResponse,
                ),
            }
            lifecycle_types = legacy_lifecycle_types.get(task_type)
            if (
                not preserve_legacy_identity
                and lifecycle_types is not None
                and self._callback_creative_dialect(result) is CreativeDialect.LEGACY
            ):
                projected = self._canonicalize_lifecycle_result(
                    TaskResult[Any](
                        status=TaskStatus.COMPLETED,
                        data=result,
                        success=True,
                        message=message,
                    ),
                    legacy_type=lifecycle_types[0],
                    canonical_type=lifecycle_types[1],
                )
                return cast(TaskResult[AdcpAsyncResponseData], projected)
            if (
                not preserve_legacy_identity
                and task_type == "list_creatives"
                and self._callback_creative_dialect(result) is CreativeDialect.LEGACY
            ):
                projected = self._canonicalize_format_read_result(
                    TaskResult[Any](
                        status=TaskStatus.COMPLETED,
                        data=result,
                        success=True,
                        message=message,
                    ),
                    legacy_type=LegacyListCreativesResponse,
                    canonical_type=ListCreativesResponse,
                    collection="creatives",
                    require_format=True,
                )
                return cast(TaskResult[AdcpAsyncResponseData], projected)
            if (
                not preserve_legacy_identity
                and task_type == "get_creative_delivery"
                and self._callback_creative_dialect(result) is CreativeDialect.LEGACY
            ):
                projected = self._canonicalize_format_read_result(
                    TaskResult[Any](
                        status=TaskStatus.COMPLETED,
                        data=result,
                        success=True,
                        message=message,
                    ),
                    legacy_type=LegacyGetCreativeDeliveryResponse,
                    canonical_type=GetCreativeDeliveryResponse,
                    collection="creatives",
                    require_format=False,
                )
                return cast(TaskResult[AdcpAsyncResponseData], projected)
            response_type = response_type_map.get(task_type)
            if response_type:
                try:
                    parsed_result: Any = parse_json_or_text(result, response_type)
                    return TaskResult[AdcpAsyncResponseData](
                        status=TaskStatus.COMPLETED,
                        data=parsed_result,
                        success=True,
                        metadata={
                            "task_id": task_id,
                            "operation_id": operation_id,
                            "timestamp": timestamp,
                            "message": message,
                        },
                    )
                except ValueError as e:
                    logger.warning(f"Failed to parse webhook result: {e}")
                    # Fall through to untyped result

        # Handle failed, input-required, or unparseable results
        # Convert status to core TaskStatus enum
        status_map = {
            GeneratedTaskStatus.completed: TaskStatus.COMPLETED,
            GeneratedTaskStatus.submitted: TaskStatus.SUBMITTED,
            GeneratedTaskStatus.working: TaskStatus.WORKING,
            GeneratedTaskStatus.failed: TaskStatus.FAILED,
            GeneratedTaskStatus.input_required: TaskStatus.NEEDS_INPUT,
        }
        task_status = status_map.get(status, TaskStatus.FAILED)

        # Extract error message from result.errors if present
        error_message: str | None = None
        if result is not None and hasattr(result, "errors"):
            errors = getattr(result, "errors", None)
            if errors and len(errors) > 0:
                first_error = errors[0]
                if hasattr(first_error, "message"):
                    error_message = first_error.message

        return TaskResult[AdcpAsyncResponseData](
            status=task_status,
            data=result,
            success=status == GeneratedTaskStatus.completed,
            error=error_message,
            metadata={
                "task_id": task_id,
                "operation_id": operation_id,
                "timestamp": timestamp,
                "message": message,
                "context_id": context_id,
            },
        )

    async def _handle_mcp_webhook(
        self,
        payload: dict[str, Any],
        task_type: str,
        operation_id: str,
        signature: str | None,
        timestamp: str | None = None,
        raw_body: bytes | str | None = None,
        preserve_legacy_identity: bool = False,
    ) -> TaskResult[AdcpAsyncResponseData]:
        """
        Handle MCP webhook delivered via HTTP POST.

        Args:
            payload: Webhook payload dict
            task_type: Task type from application routing
            operation_id: Operation identifier from application routing
            signature: HMAC-SHA256 signature from X-AdCP-Signature. Required
                when ``webhook_secret`` configures the deprecated HMAC fallback.
            timestamp: Unix timestamp from X-AdCP-Timestamp. Required with the
                deprecated HMAC fallback.
            raw_body: Raw HTTP request body. Required with the deprecated HMAC
                fallback so the authenticated bytes are the bytes processed.

        Returns:
            TaskResult with parsed task-specific response data

        Raises:
            ADCPWebhookSignatureError: If signature verification fails
            ValidationError: If payload doesn't match McpWebhookPayload schema
        """
        from adcp.types.generated_poc.core.mcp_webhook_payload import McpWebhookPayload

        # Signed MCP webhooks are the secure default. Receiving without a
        # verifier requires an explicit compatibility opt-in so a missing
        # secret cannot silently turn a public endpoint into an unauthenticated
        # callback receiver.
        if self.webhook_secret:
            if not signature or not timestamp:
                raise ADCPWebhookSignatureError(
                    "Webhook signature and timestamp headers are required"
                )
            if not self._verify_webhook_signature(payload, signature, timestamp, raw_body):
                logger.warning(
                    f"Webhook signature verification failed for agent {self.agent_config.id}"
                )
                raise ADCPWebhookSignatureError("Invalid webhook signature")
            if raw_body is None:  # Defensive type narrowing; verifier rejects this above.
                raise ADCPWebhookSignatureError("Signed webhook raw body is required")
            try:
                authenticated_payload = json.loads(raw_body)
            except (TypeError, ValueError, UnicodeDecodeError) as exc:
                raise ADCPWebhookSignatureError("Invalid signed webhook body") from exc
            if not isinstance(authenticated_payload, dict):
                raise ADCPWebhookSignatureError("Signed webhook body must be a JSON object")
            # Process the bytes that were authenticated, not a separately
            # supplied parsed object that middleware could have transformed.
            payload = cast(dict[str, Any], authenticated_payload)
        elif self.allow_unauthenticated_webhooks is not True:
            raise ADCPWebhookSignatureError(
                "MCP webhook cannot be authenticated because webhook_secret is not configured; "
                "use WebhookReceiver for RFC 9421 callbacks, configure a shared secret only for "
                "an explicitly selected legacy HMAC registration, or set "
                "allow_unauthenticated_webhooks=True only for receivers isolated from "
                "untrusted networks"
            )

        # Select the canonical/legacy surface from the body only after signed
        # callbacks have been replaced by their authenticated raw bytes.
        payload_task_type = payload.get("task_type")
        if not preserve_legacy_identity and payload_task_type in _LEGACY_ONLY_CREATIVE_TASKS:
            raise ValueError(
                f"{payload_task_type} webhook payloads carry legacy creative identity; use "
                "handle_webhook_legacy()"
            )

        # Validate and parse MCP webhook payload
        webhook = McpWebhookPayload.model_validate(payload)
        authenticated_task_type = webhook.task_type.value
        authenticated_operation_id = webhook.operation_id

        if preserve_legacy_identity:
            if authenticated_task_type not in _LEGACY_CREATIVE_TASKS:
                raise ValueError(
                    f"{authenticated_task_type} is not a legacy-only callback; use "
                    "handle_webhook()"
                )

        # Emit activity for monitoring
        self._emit_activity(
            Activity(
                type=ActivityType.WEBHOOK_RECEIVED,
                operation_id=authenticated_operation_id,
                agent_id=self.agent_config.id,
                task_type=authenticated_task_type,
                timestamp=datetime.now(timezone.utc).isoformat(),
                metadata={
                    "task_id": webhook.task_id,
                    "status": webhook.status.value,
                    "protocol": "mcp",
                },
            )
        )

        # Extract fields and parse result
        return self._parse_webhook_result(
            task_id=webhook.task_id,
            task_type=authenticated_task_type,
            operation_id=authenticated_operation_id,
            status=webhook.status,
            result=webhook.result,
            timestamp=webhook.timestamp,
            message=webhook.message,
            context_id=webhook.context_id,
            preserve_legacy_identity=preserve_legacy_identity,
        )

    async def _handle_a2a_webhook(
        self,
        payload: Task | TaskStatusUpdateEvent,
        task_type: str,
        operation_id: str,
        preserve_legacy_identity: bool = False,
    ) -> TaskResult[AdcpAsyncResponseData]:
        """
        Handle A2A webhook delivered through Task or TaskStatusUpdateEvent.

        Per A2A specification:
        - Terminated statuses (completed, failed): Payload is Task with artifacts[].parts[]
        - Intermediate statuses (working, input-required, submitted):
        Payload is TaskStatusUpdateEvent with status.message.parts[]

        Args:
            payload: A2A Task or TaskStatusUpdateEvent object
            task_type: Task type from application routing
            operation_id: Operation identifier from application routing

        Returns:
            TaskResult with parsed task-specific response data

        Note:
            Signature verification is NOT applicable for A2A webhooks
            as they arrive through authenticated A2A connections, not HTTP.
        """
        from a2a import types as _pb
        from google.protobuf.json_format import MessageToDict as _MessageToDict

        def _a2a_part_data_dict(part: _pb.Part) -> Any:
            if part.WhichOneof("content") != "data":
                return None
            return _MessageToDict(part.data)

        def _a2a_part_text(part: _pb.Part) -> str | None:
            if part.WhichOneof("content") != "text":
                return None
            return part.text

        def _a2a_state_to_string(state_value: int) -> str:
            """Map ``TaskState`` int → spec string (``TASK_STATE_COMPLETED`` → ``completed``)."""
            name = _pb.TaskState.Name(state_value)
            if name.startswith("TASK_STATE_"):
                return name[len("TASK_STATE_") :].lower().replace("_", "-")
            return name.lower()

        def _a2a_timestamp(ts: Any) -> datetime | str:
            """Convert a proto Timestamp (or string) to datetime/ISO string."""
            if ts is None:
                return datetime.now(timezone.utc)
            if isinstance(ts, str):
                return ts or datetime.now(timezone.utc)
            try:
                return cast(datetime, ts.ToDatetime().replace(tzinfo=timezone.utc))
            except AttributeError:
                return datetime.now(timezone.utc)

        adcp_data: Any = None
        text_message: str | None = None
        task_id: str
        context_id: str | None
        status_state: str
        timestamp: datetime | str

        # Type detection and extraction based on payload type
        if isinstance(payload, TaskStatusUpdateEvent):
            task_id = payload.task_id
            context_id = payload.context_id or None
            has_status = payload.HasField("status")
            status_state = _a2a_state_to_string(payload.status.state) if has_status else "failed"
            timestamp = (
                _a2a_timestamp(payload.status.timestamp)
                if has_status and payload.status.HasField("timestamp")
                else datetime.now(timezone.utc)
            )

            if has_status and payload.status.HasField("message") and payload.status.message.parts:
                data_parts = [
                    d
                    for d in (_a2a_part_data_dict(p) for p in payload.status.message.parts)
                    if d is not None
                ]
                if data_parts:
                    adcp_data = data_parts[-1]
                    if isinstance(adcp_data, dict) and "response" in adcp_data:
                        adcp_data = adcp_data["response"]

                for part in payload.status.message.parts:
                    text = _a2a_part_text(part)
                    if text is not None:
                        text_message = text
                        break

        else:
            task_id = payload.id
            context_id = payload.context_id or None
            has_status = payload.HasField("status")
            status_state = _a2a_state_to_string(payload.status.state) if has_status else "failed"
            timestamp = (
                _a2a_timestamp(payload.status.timestamp)
                if has_status and payload.status.HasField("timestamp")
                else datetime.now(timezone.utc)
            )

            if payload.artifacts:
                target_artifact = payload.artifacts[-1]
                if target_artifact.parts:
                    data_parts = [
                        d
                        for d in (_a2a_part_data_dict(p) for p in target_artifact.parts)
                        if d is not None
                    ]
                    if data_parts:
                        adcp_data = data_parts[-1]
                        if isinstance(adcp_data, dict) and "response" in adcp_data:
                            adcp_data = adcp_data["response"]

                    for part in target_artifact.parts:
                        text = _a2a_part_text(part)
                        if text is not None:
                            text_message = text
                            break

        # Map A2A status.state to GeneratedTaskStatus enum
        status_map = {
            "completed": GeneratedTaskStatus.completed,
            "submitted": GeneratedTaskStatus.submitted,
            "working": GeneratedTaskStatus.working,
            "failed": GeneratedTaskStatus.failed,
            "input-required": GeneratedTaskStatus.input_required,
            "input_required": GeneratedTaskStatus.input_required,  # Handle both formats
        }
        mapped_status = status_map.get(status_state, GeneratedTaskStatus.failed)

        # Emit activity for monitoring
        self._emit_activity(
            Activity(
                type=ActivityType.WEBHOOK_RECEIVED,
                operation_id=operation_id,
                agent_id=self.agent_config.id,
                task_type=task_type,
                timestamp=datetime.now(timezone.utc).isoformat(),
                metadata={
                    "task_id": task_id,
                    "protocol": "a2a",
                    "payload_type": (
                        "TaskStatusUpdateEvent"
                        if isinstance(payload, TaskStatusUpdateEvent)
                        else "Task"
                    ),
                },
            )
        )

        # Parse and return typed result by passing extracted fields directly
        return self._parse_webhook_result(
            task_id=task_id,
            task_type=task_type,
            operation_id=operation_id,
            status=mapped_status,
            result=adcp_data,
            timestamp=timestamp,
            message=text_message,
            context_id=context_id,
            preserve_legacy_identity=preserve_legacy_identity,
        )

    async def handle_webhook(
        self,
        payload: dict[str, Any] | Task | TaskStatusUpdateEvent,
        task_type: str,
        operation_id: str,
        signature: str | None = None,
        timestamp: str | None = None,
        raw_body: bytes | str | None = None,
    ) -> TaskResult[AdcpAsyncResponseData]:
        """
        Handle incoming webhook and return typed result.

        This method provides a unified interface for handling webhooks from both
        MCP and A2A protocols:

        - MCP Webhooks: HTTP POST with dict payload; the deprecated HMAC fallback
          requires a signature, timestamp, and raw body
        - A2A Webhooks: Task or TaskStatusUpdateEvent objects based on status

        The method automatically detects the protocol type and routes to the
        appropriate handler. Both protocols return a consistent TaskResult
        structure with typed AdCP response data.

        Args:
            payload: Webhook payload - one of:
                - dict[str, Any]: MCP webhook payload from HTTP POST
                - Task: A2A webhook for terminated statuses (completed, failed)
                - TaskStatusUpdateEvent: A2A webhook for intermediate statuses
                  (working, input-required, submitted)
            task_type: Task type from application routing for A2A callbacks. For
                MCP callbacks, the validated payload's authenticated ``task_type``
                controls parsing and activity correlation.
            operation_id: Operation identifier from application routing. For MCP
                callbacks, the authenticated payload value controls correlation
                when present; this argument is a compatibility fallback for old
                payloads that omit it.
            signature: HMAC-SHA256 signature from X-AdCP-Signature. Required when
                ``webhook_secret`` configures the deprecated HMAC fallback and
                ignored for A2A callbacks.
            timestamp: Unix timestamp from X-AdCP-Timestamp. Required with the
                deprecated HMAC fallback and ignored for A2A callbacks.
            raw_body: Raw HTTP request body captured before JSON parsing. Required
                with the deprecated HMAC fallback and ignored for A2A callbacks.

        Returns:
            TaskResult with parsed task-specific response data. The structure
            is identical regardless of protocol.

        Raises:
            ADCPWebhookSignatureError: If MCP signature verification fails
            ValidationError: If MCP payload doesn't match WebhookPayload schema

        Note:
            AdCP-conformant public MCP endpoints should use
            :class:`adcp.webhooks.WebhookReceiver`, which verifies RFC 9421,
            deduplicates retries, and parses the authenticated body. This method's
            HMAC mode exists only for explicitly selected legacy registrations.

        Examples:
            MCP webhook (HTTP endpoint):
            >>> @app.post("/webhook/{task_type}/{agent_id}/{operation_id}")
            >>> async def webhook_handler(task_type: str, operation_id: str, request: Request):
            >>>     raw_body = await request.body()
            >>>     payload = json.loads(raw_body)
            >>>     signature = request.headers.get("X-AdCP-Signature")
            >>>     timestamp = request.headers.get("X-AdCP-Timestamp")
            >>>     result = await client.handle_webhook(
            >>>         payload, task_type, operation_id, signature, timestamp,
            >>>         raw_body=raw_body,
            >>>     )
            >>>     if result.success:
            >>>         print(f"Task completed: {result.data}")

            A2A webhook with Task (terminated status):
            >>> async def on_task_completed(task: Task):
            >>>     # Extract task_type and operation_id from your app's task tracking
            >>>     task_type = your_task_registry.get_type(task.id)
            >>>     operation_id = your_task_registry.get_operation_id(task.id)
            >>>     result = await client.handle_webhook(
            >>>         task, task_type, operation_id
            >>>     )
            >>>     if result.success:
            >>>         print(f"Task completed: {result.data}")

            A2A webhook with TaskStatusUpdateEvent (intermediate status):
            >>> async def on_task_update(event: TaskStatusUpdateEvent):
            >>>     # Extract task_type and operation_id from your app's task tracking
            >>>     task_type = your_task_registry.get_type(event.task_id)
            >>>     operation_id = your_task_registry.get_operation_id(event.task_id)
            >>>     result = await client.handle_webhook(
            >>>         event, task_type, operation_id
            >>>     )
            >>>     if result.status == GeneratedTaskStatus.working:
            >>>         print(f"Task still working: {result.metadata.get('message')}")
        """
        if (
            isinstance(payload, (Task, TaskStatusUpdateEvent))
            and task_type in _LEGACY_ONLY_CREATIVE_TASKS
        ):
            raise ValueError(
                f"{task_type} webhook payloads carry legacy creative identity; use "
                "handle_webhook_legacy()"
            )
        result = await self._dispatch_webhook(
            payload,
            task_type,
            operation_id,
            signature,
            timestamp,
            raw_body,
            preserve_legacy_identity=False,
        )
        sanitized = strip_legacy_creative_identity(result.model_dump(mode="python"))
        return TaskResult[AdcpAsyncResponseData].model_validate(sanitized)

    async def handle_webhook_legacy(
        self,
        payload: dict[str, Any] | Task | TaskStatusUpdateEvent,
        task_type: str,
        operation_id: str,
        signature: str | None = None,
        timestamp: str | None = None,
        raw_body: bytes | str | None = None,
    ) -> TaskResult[AdcpAsyncResponseData]:
        """Parse a callback for a task whose protocol shape is explicitly legacy-only."""

        if isinstance(payload, (Task, TaskStatusUpdateEvent)) and task_type not in (
            _LEGACY_CREATIVE_TASKS
        ):
            raise ValueError(f"{task_type} is not a legacy-only callback; use handle_webhook()")
        self._warn_legacy_creative_api("handle_webhook_legacy")
        return await self._dispatch_webhook(
            payload,
            task_type,
            operation_id,
            signature,
            timestamp,
            raw_body,
            preserve_legacy_identity=True,
        )

    async def _dispatch_webhook(
        self,
        payload: dict[str, Any] | Task | TaskStatusUpdateEvent,
        task_type: str,
        operation_id: str,
        signature: str | None,
        timestamp: str | None,
        raw_body: bytes | str | None,
        *,
        preserve_legacy_identity: bool,
    ) -> TaskResult[AdcpAsyncResponseData]:
        """Route a callback after the public canonical/legacy boundary is selected."""

        # Detect protocol type and route to appropriate handler
        if isinstance(payload, (Task, TaskStatusUpdateEvent)):
            # A2A webhook (Task or TaskStatusUpdateEvent)
            return await self._handle_a2a_webhook(
                payload,
                task_type,
                operation_id,
                preserve_legacy_identity,
            )
        else:
            # MCP webhook (dict payload)
            return await self._handle_mcp_webhook(
                payload,
                task_type,
                operation_id,
                signature,
                timestamp,
                raw_body,
                preserve_legacy_identity,
            )

Client for interacting with a single AdCP agent.

Initialize ADCP client for a single agent.

Args
-----=
agent_config
Agent configuration
webhook_url_template
Template for webhook URLs with {agent_id}, {task_type}, {operation_id}
webhook_secret
Shared secret for the deprecated HMAC-SHA256 webhook fallback. Configure this only when the registration explicitly selected legacy HMAC; conformant public endpoints should use :class:WebhookReceiver for RFC 9421 verification.
allow_unauthenticated_webhooks
Explicit compatibility escape for accepting unsigned MCP webhooks when webhook_secret is not configured. Defaults to False so public webhook receivers fail closed. Only enable this for endpoints that cannot be reached from an untrusted network. A2A webhook handling is unaffected.
on_activity
Callback for activity events
webhook_timestamp_tolerance
Maximum age (in seconds) for webhook timestamps. Webhooks with timestamps older than this or more than this far in the future are rejected. Defaults to 300 (5 minutes).
capabilities_ttl
Time-to-live in seconds for cached capabilities (default: 1 hour)
validate_features
When True, automatically check that the seller supports required features before making task calls (e.g., sync_audiences requires audience_targeting). Requires capabilities to have been fetched first.
strict_idempotency
When True, verify the seller declared adcp.idempotency.replay_ttl_seconds in capabilities before any mutating call. Fetches capabilities lazily on first use. Raises IdempotencyUnsupportedError if the declaration is missing — sellers that don't declare it provide no retry-safety guarantee per AdCP #2315. Defaults to False for backward compatibility.
signing
Optional RFC 9421 request-signing config. When provided, the client automatically attaches Signature / Signature-Input / Content-Digest headers to operations the seller's request_signing capability lists in required_for, warn_for, or supported_for. The seller's covers_content_digest policy determines whether the body is bound to the signature. Generate a key with adcp-keygen and publish the public JWK at your jwks_uri. Supported on both A2A and MCP (mcp_transport="streamable_http"); SSE-transport MCP logs a warning and falls through unsigned.
validation
Schema-driven validation modes for outgoing requests and incoming responses against the bundled AdCP JSON schemas. Defaults (matching the TS port): requests in warn mode (drift logged but not blocked — partial payloads in error-path tests still work) and responses in strict mode (agent drift fails the task). ADCP_VALIDATION_MODE=strict|warn|off overrides both sides at call time (matches the TS port); ADCP_ENV set to production / prod flips only the response default to warn. Generic ENV / ENVIRONMENT / PYTHON_ENV are deliberately ignored — they collide with unrelated tooling. Storyboards and compliance runners that want hard-stop enforcement everywhere pass validation=ValidationHookConfig(requests="strict", responses="strict"); high-throughput callers can set either side to "off" to skip the validator entirely with zero overhead.
context_id

A2A-only. Seed the A2A conversation context. Pass a previously-returned context_id to resume a session across process restarts, or a self-assigned UUID to name the session with your own correlation key (the ADK server honors buyer-proposed ids). If omitted, the server mints one on the first message and this client auto-retains it for subsequent calls. Read the current value via client.context_id; call client.reset_context() to start a fresh conversation. Rule of thumb: one ADCPClient per A2A conversation — if a buyer has multiple concurrent briefs with the same agent, construct one client per brief rather than sharing.

For HITL flows that can span a process restart mid-task, use checkpoint() / from_checkpoint() instead of persisting context_id alone — full resume state is both context_id AND active_task_id.

Raises TypeError if passed with a non-A2A protocol.

force_a2a_version

A2A-only. Pin the A2A transport version (e.g. "0.3", "1.0") by filtering the peer's advertised supported_interfaces to entries whose protocol_version matches. Not for AdCP protocol pinning — see adcp_version for that. Intended for tests or for forcing a 0.3-speaking path against a dual-advertising peer. Raises :class:ADCPConnectionError on the first call if no advertised interface matches. None (default) lets the SDK's ClientFactory pick the most capable transport the peer supports. Use :attr:a2a_protocol_versions to probe what a peer advertises before pinning.

Raises TypeError if passed with a non-A2A protocol.

adcp_version

AdCP protocol release this client speaks (release-precision string, e.g. "3.0", "3.1", "3.1-beta"). Stripe-style per-instance pin: the value is sent as adcp_version on every outbound request and selects creative dialect behavior. None (default) resolves to the SDK's compile-time pin (ADCP_VERSION packaged with the wheel). Cross-major pins raise :class:ConfigurationError at construction; install the SDK major that targets your wire version instead. Patch-precision strings ("3.0.1") and build metadata ("3.0.1+canary") are accepted at construction but normalized to release-precision before wire emission per the spec — patches and build metadata are not part of the negotiation contract. get_adcp_version() returns the normalized form.

Caller-supplied adcp_version on a per-call params dict wins over the constructor pin: the enricher is the default, not an override.

Migration from adcp_major_version (legacy integer wire field): generated request types still expose adcp_major_version: int | None from the pre-#3493 schema. Both fields will coexist on the wire through 3.x; servers prefer the new adcp_version when both are present. Stop populating adcp_major_version on request models once your seller advertises 3.1 in supported_versions.

server_version

AdCP wire shape the seller speaks. Most adopters leave this None — the SDK assumes a v3 seller and the seller's /.well-known/agent-card.json is the canonical source of truth once a probe-and-cache path lands.

Pin explicitly when:

  • You're talking to a known-legacy seller (e.g. server_version="3.0"). Canonical discovery results are upgraded for application code and canonical writes are downgraded only through preserved or explicit routes.
  • You want telemetry to attribute outbound traffic to a specific server-side version regardless of what the seller advertises.

Retrieve the current value via :meth:get_server_version.

Static methods

def from_checkpoint(agent_config: AgentConfig,
state: Checkpoint,
**kwargs: Any) ‑> ADCPClient

Rehydrate an ADCPClient from a prior checkpoint().

Restores both context_id and active_task_id so a process restart mid-input-required can resume the same task, not orphan it. Accepts the same keyword arguments as __init__ (signing, strict_idempotency, etc.) — the checkpoint only carries session-resume state; operational config is re-supplied by the caller.

Raises ValueError if the checkpoint's agent_id doesn't match agent_config.id — a checkpoint minted for Agent A must not be restored onto Agent B, or the client will leak Agent A's opaque session ids to Agent B on the next message.

Raises TypeError on a non-A2A agent_config if the checkpoint carries a non-empty context_id or active_task_id — session-resume state on a protocol that doesn't support it would be silently dropped, masking bugs. An empty/absent checkpoint round-trips cleanly on any protocol.

def from_mcp_client(client: ClientSession,
*,
agent_id: str | None = None,
validation: ValidationHookConfig | None = None,
capabilities_ttl: float = 3600.0,
validate_features: bool = False,
strict_idempotency: bool = False)

Create an ADCPClient wrapping a pre-connected MCP ClientSession.

Parity with JS AgentClient.fromMCPClient() (v5.19.0). The primary use case is compliance test fleets that wire a full ADCPClient against an in-process MCP server without standing up a loopback HTTP server.

Warning -----= The returned client's close() and async with __aexit__ are no-ops — the caller owns the injected session and is responsible for closing it. Code that relies on async with ADCPClient.from_mcp_client(...) as c: to clean up the session will leak the session.

Webhook delivery and on_activity callbacks are not wired on the in-process path — there is no HTTP transport for the seller to call back through. Don't pass these to the factory (they're absent from the signature on purpose).

If the injected session has not been initialized (await session.initialize()), the first tool call surfaces as an opaque MCP protocol error in TaskResult.error. The factory does not initialize for you — verify before calling.

Session lifecycle: the caller owns the session — close() and async with exit on the returned client are no-ops. Use your own AsyncExitStack to scope both the transport and the client::

import contextlib
from mcp import ClientSession
from mcp.shared.memory import create_client_server_memory_streams

async with contextlib.AsyncExitStack() as stack:
    (c_read, c_write), (s_read, s_write) = await stack.enter_async_context(
        create_client_server_memory_streams()
    )
    # wire your in-process server to (s_read, s_write) here
    session = await stack.enter_async_context(
        ClientSession(c_read, c_write)
    )
    await session.initialize()
    # close() is a no-op on injected sessions; no stack.enter_async_context needed.
    adcp_client = ADCPClient.from_mcp_client(session, agent_id="test-seller")
    result = await adcp_client.get_products(GetProductsRequest(...))

Note -----= Request signing is not supported on the injected-session path — the signing hook is wired into the HTTP transport layer that is bypassed here. signing= is intentionally absent from this factory's parameters.

Args
-----=
client
A pre-connected mcp.ClientSession whose initialize() has already been awaited.
agent_id
Identifier for the wrapped agent used in log messages and error objects. Defaults to a unique in-process-XXXXXXXX token; set this explicitly when running multiple in-process agents concurrently so log lines are distinguishable.
validation
Schema-validation modes (same as __init__).
strict_idempotency
Verify seller declared idempotency support before each mutating call (same as __init__).
validate_features
Gate tool calls on fetched capability declarations (same as __init__).
capabilities_ttl
TTL for the capability cache in seconds (same as __init__).

Returns -----= A fully configured ADCPClient backed by the injected session.

Instance variables

prop a2a_protocol_versions : list[str] | None
Expand source code
@property
def a2a_protocol_versions(self) -> list[str] | None:
    """A2A ``protocol_version`` strings the peer advertises, sorted.

    Lazily populated after the first operation that fetches the
    peer's ``AgentCard`` (``fetch_capabilities``, ``list_tools``,
    ``get_agent_info``, or any skill-call). Returns ``None`` before
    the card has been fetched so callers can distinguish "not yet
    known" from "peer advertises nothing" (empty list). Returns
    ``None`` for non-A2A clients.

    Useful for probing which wire version a peer speaks — buyers
    running alongside both 0.3-era and 1.0-era agents can use this
    to confirm what they're talking to.
    """
    if isinstance(self.adapter, A2AAdapter):
        return self.adapter.a2a_protocol_versions
    return None

A2A protocol_version strings the peer advertises, sorted.

Lazily populated after the first operation that fetches the peer's AgentCard (fetch_capabilities, list_tools, get_agent_info, or any skill-call). Returns None before the card has been fetched so callers can distinguish "not yet known" from "peer advertises nothing" (empty list). Returns None for non-A2A clients.

Useful for probing which wire version a peer speaks — buyers running alongside both 0.3-era and 1.0-era agents can use this to confirm what they're talking to.

prop active_task_id : str | None
Expand source code
@property
def active_task_id(self) -> str | None:
    """A2A task_id the next send must echo to resume the same task.

    Set when the last A2A response was non-terminal
    (``input-required``, ``working``, ``submitted``,
    ``auth-required``). The adapter echoes this id on the next
    outbound message so the server resumes the same task. Clears
    automatically when the task reaches a terminal state.

    Full resume state is *both* ``context_id`` and
    ``active_task_id`` — persist both (or use ``checkpoint()``) to
    survive a process restart mid-HITL without orphaning the task.

    Returns ``None`` for non-A2A clients.
    """
    if isinstance(self.adapter, A2AAdapter):
        return self.adapter.active_task_id
    return None

A2A task_id the next send must echo to resume the same task.

Set when the last A2A response was non-terminal (input-required, working, submitted, auth-required). The adapter echoes this id on the next outbound message so the server resumes the same task. Clears automatically when the task reaches a terminal state.

Full resume state is both context_id and active_task_id — persist both (or use checkpoint()) to survive a process restart mid-HITL without orphaning the task.

Returns None for non-A2A clients.

prop capabilities : GetAdcpCapabilitiesResponse | None
Expand source code
@property
def capabilities(self) -> GetAdcpCapabilitiesResponse | None:
    """Return cached capabilities, or None if not yet fetched."""
    return self._capabilities

Return cached capabilities, or None if not yet fetched.

prop context_id : str | None
Expand source code
@property
def context_id(self) -> str | None:
    """Current A2A conversation context_id.

    Reads the context_id currently associated with this client: the
    value assigned by the A2A server (auto-captured from the most
    recent response) or the one seeded via the constructor or
    ``reset_context()``. Returns ``None`` before the first A2A call
    in a fresh conversation, or for clients on non-A2A protocols —
    reads are lenient across protocols so generic code can probe
    ``if client.context_id: ...`` safely. Writes (constructor kwarg,
    ``reset_context``) raise on non-A2A because the operation has no
    meaning there.

    Not safe for concurrent calls on the same client — the adapter
    mutates this on every response. Rule of thumb: one ADCPClient
    per A2A conversation.

    For simple completed-task resume, persist this value and pass
    it to ``ADCPClient(context_id=...)``. For HITL flows that may
    restart mid-``input-required``, use ``checkpoint()`` /
    ``from_checkpoint()`` — full resume state is both this id AND
    ``active_task_id``.
    """
    if isinstance(self.adapter, A2AAdapter):
        return self.adapter.context_id
    return None

Current A2A conversation context_id.

Reads the context_id currently associated with this client: the value assigned by the A2A server (auto-captured from the most recent response) or the one seeded via the constructor or reset_context(). Returns None before the first A2A call in a fresh conversation, or for clients on non-A2A protocols — reads are lenient across protocols so generic code can probe if client.context_id: ... safely. Writes (constructor kwarg, reset_context) raise on non-A2A because the operation has no meaning there.

Not safe for concurrent calls on the same client — the adapter mutates this on every response. Rule of thumb: one ADCPClient per A2A conversation.

For simple completed-task resume, persist this value and pass it to ADCPClient(context_id=...). For HITL flows that may restart mid-input-required, use checkpoint() / from_checkpoint() — full resume state is both this id AND active_task_id.

prop feature_resolverFeatureResolver | None
Expand source code
@property
def feature_resolver(self) -> FeatureResolver | None:
    """Return the FeatureResolver for cached capabilities, or None."""
    return self._feature_resolver

Return the FeatureResolver for cached capabilities, or None.

Methods

async def accept_proposal(self,
request: AcceptProposalRequest) ‑> TaskResult[Union[AcceptProposalResponse5, AcceptProposalResponse6, AcceptProposalResponse7]]
Expand source code
async def accept_proposal(
    self, request: AcceptProposalRequest
) -> TaskResult[AcceptProposalResponse]:
    """Accept a seller proposal and create its media buy."""
    return cast(
        TaskResult[AcceptProposalResponse],
        await self._execute_typed_task("accept_proposal", request, AcceptProposalResponse),
    )

Accept a seller proposal and create its media buy.

async def acquire_rights(self,
request: AcquireRightsRequest) ‑> TaskResult[Union[AcquireRightsResponse1AcquireRightsResponse2AcquireRightsResponse3AcquireRightsResponse4]]
Expand source code
async def acquire_rights(
    self,
    request: AcquireRightsRequest,
) -> TaskResult[AcquireRightsResponse]:
    """Acquire rights for brand content usage.

    Binding contractual request to license rights for a campaign.
    Returns credentials for generating rights-cleared content.

    Args:
        request: Request with rights_id, pricing_option_id, buyer,
            campaign, and revocation_webhook.

    Returns:
        TaskResult containing AcquireRightsResponse (acquired,
        pending_approval, rejected, or error).
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="acquire_rights",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.acquire_rights(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="acquire_rights",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, AcquireRightsResponse)

Acquire rights for brand content usage.

Binding contractual request to license rights for a campaign. Returns credentials for generating rights-cleared content.

Args
-----=
request
Request with rights_id, pricing_option_id, buyer, campaign, and revocation_webhook.

Returns -----= TaskResult containing AcquireRightsResponse (acquired, pending_approval, rejected, or error).

async def activate_signal(self,
request: ActivateSignalRequest) ‑> TaskResult[Union[ActivateSignalResponse1ActivateSignalResponse2]]
Expand source code
async def activate_signal(
    self,
    request: ActivateSignalRequest,
) -> TaskResult[ActivateSignalResponse]:
    """
    Activate Signal.

    Args:
        request: Request parameters

    Returns:
        TaskResult containing ActivateSignalResponse
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="activate_signal",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.activate_signal(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="activate_signal",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, ActivateSignalResponse)

Activate Signal.

Args
-----=
request
Request parameters

Returns -----= TaskResult containing ActivateSignalResponse

async def build_creative_legacy(self,
request: BuildCreativeRequest) ‑> TaskResult[Union[BuildCreativeResponse1BuildCreativeResponse2BuildCreativeResponse3BuildCreativeResponse4BuildCreativeResponse5BuildCreativeResponse6]]
Expand source code
async def build_creative_legacy(
    self,
    request: LegacyBuildCreativeRequest,
) -> TaskResult[LegacyBuildCreativeResponse]:
    """
    Generate production-ready creative assets.

    Requests the creative agent to build final deliverable assets in the target
    format (e.g., VAST, DAAST, HTML5). This is typically called after previewing
    and approving a creative manifest.

    Args:
        request: Creative build parameters including:
            - manifest: Creative manifest with brand info and content
            - target_format_id: Desired output format identifier
            - inputs: Optional user-provided inputs for template variables
            - deployment: Platform or agent deployment configuration

    Returns:
        TaskResult containing BuildCreativeResponse with:
            - assets: Production-ready creative files (URLs or inline content)
            - format_id: The generated format identifier
            - manifest: The creative manifest used for generation
            - metadata: Additional platform-specific details

    Example:
        >>> from adcp import ADCPClient, LegacyBuildCreativeRequest
        >>> client = ADCPClient(agent_config)
        >>> request = LegacyBuildCreativeRequest(
        ...     manifest=creative_manifest,
        ...     target_format_id="vast_2.0",
        ...     inputs={"duration": 30}
        ... )
        >>> result = await client.build_creative_legacy(request)
        >>> if result.success:
        ...     vast_url = result.data.assets[0].url
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="build_creative",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.build_creative(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="build_creative",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    self._warn_legacy_creative_api("build_creative_legacy")
    return self.adapter._parse_response(raw_result, LegacyBuildCreativeResponse)

Generate production-ready creative assets.

Requests the creative agent to build final deliverable assets in the target format (e.g., VAST, DAAST, HTML5). This is typically called after previewing and approving a creative manifest.

Args
-----=
request
Creative build parameters including: - manifest: Creative manifest with brand info and content - target_format_id: Desired output format identifier - inputs: Optional user-provided inputs for template variables - deployment: Platform or agent deployment configuration

Returns -----= TaskResult containing BuildCreativeResponse with: - assets: Production-ready creative files (URLs or inline content) - format_id: The generated format identifier - manifest: The creative manifest used for generation - metadata: Additional platform-specific details

Example -----=

>>> from adcp import ADCPClient, LegacyBuildCreativeRequest
>>> client = ADCPClient(agent_config)
>>> request = LegacyBuildCreativeRequest(
...     manifest=creative_manifest,
...     target_format_id="vast_2.0",
...     inputs={"duration": 30}
... )
>>> result = await client.build_creative_legacy(request)
>>> if result.success:
...     vast_url = result.data.assets[0].url
async def buy_products(self,
request: BuyProductsRequest) ‑> TaskResult[Union[BuyProductsResponse5, BuyProductsResponse6, BuyProductsResponse7]]
Expand source code
async def buy_products(self, request: BuyProductsRequest) -> TaskResult[BuyProductsResponse]:
    """Commit a direct product purchase."""
    return cast(
        TaskResult[BuyProductsResponse],
        await self._execute_typed_task("buy_products", request, BuyProductsResponse),
    )

Commit a direct product purchase.

async def calibrate_content(self, request: CalibrateContentRequest) ‑> TaskResult[Union[CalibrateContentResponse1CalibrateContentResponse2]]
Expand source code
async def calibrate_content(
    self,
    request: CalibrateContentRequest,
) -> TaskResult[CalibrateContentResponse]:
    """
    Calibrate content against standards.

    Evaluates content (artifact or URL) against configured standards to
    determine suitability for ad placement.

    Args:
        request: Request parameters including content to evaluate

    Returns:
        TaskResult containing CalibrateContentResponse with verdict
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="calibrate_content",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.calibrate_content(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="calibrate_content",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, CalibrateContentResponse)

Calibrate content against standards.

Evaluates content (artifact or URL) against configured standards to determine suitability for ad placement.

Args
-----=
request
Request parameters including content to evaluate

Returns -----= TaskResult containing CalibrateContentResponse with verdict

async def check_governance(self,
request: CheckGovernanceRequest3) ‑> TaskResult[CheckGovernanceResponse]
Expand source code
async def check_governance(
    self,
    request: CheckGovernanceRequest,
) -> TaskResult[CheckGovernanceResponse]:
    """Check a proposed or committed action against campaign governance."""
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="check_governance",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.check_governance(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="check_governance",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, CheckGovernanceResponse)

Check a proposed or committed action against campaign governance.

def checkpoint(self) ‑> Checkpoint
Expand source code
def checkpoint(self) -> Checkpoint:
    """Return the minimal state needed to resume this A2A session.

    Full resume for HITL / multi-turn flows requires *both*
    ``context_id`` (which conversation) AND ``active_task_id``
    (which in-flight task to echo). Persisting only ``context_id``
    reconnects to the right conversation but orphans the pending
    task server-side — the next send starts a new task under the
    same context, and the original ``input-required`` task is
    abandoned.

    The returned dict also carries ``agent_id`` so a later
    ``from_checkpoint`` call against a different ``AgentConfig``
    fails loudly instead of sending one agent's session ids to
    another.

    Pair with ``ADCPClient.from_checkpoint(agent_config, state)``.

    Returns a fully-populated ``Checkpoint`` on non-A2A clients
    with ``context_id``/``active_task_id`` set to ``None``, so
    generic persist-and-restore code can call this without
    branching on protocol.
    """
    return Checkpoint(
        agent_id=self.agent_config.id,
        context_id=self.context_id,
        active_task_id=self.active_task_id,
    )

Return the minimal state needed to resume this A2A session.

Full resume for HITL / multi-turn flows requires both context_id (which conversation) AND active_task_id (which in-flight task to echo). Persisting only context_id reconnects to the right conversation but orphans the pending task server-side — the next send starts a new task under the same context, and the original input-required task is abandoned.

The returned dict also carries agent_id so a later from_checkpoint call against a different AgentConfig fails loudly instead of sending one agent's session ids to another.

Pair with ADCPClient.from_checkpoint()(agent_config, state).

Returns a fully-populated Checkpoint on non-A2A clients with context_id/active_task_id set to None, so generic persist-and-restore code can call this without branching on protocol.

async def close(self) ‑> None
Expand source code
async def close(self) -> None:
    """Close the adapter and clean up resources."""
    if hasattr(self.adapter, "close"):
        logger.debug(f"Closing adapter for agent {self.agent_config.id}")
        await self.adapter.close()

Close the adapter and clean up resources.

async def close_mcp_session(self, session_id: str | None = None) ‑> None
Expand source code
async def close_mcp_session(self, session_id: str | None = None) -> None:
    """Explicitly terminate a stateful MCP Streamable HTTP session.

    This sends ``DELETE`` to the configured MCP endpoint with the
    ``Mcp-Session-Id`` header. When ``session_id`` is omitted, the
    SDK-managed current session is closed. It is only valid for MCP
    agents using ``mcp_transport="streamable_http"``.
    """
    if not isinstance(self.adapter, MCPAdapter):
        raise TypeError(
            "close_mcp_session is only supported for MCP clients; "
            f"got {self.agent_config.protocol}"
        )
    await self.adapter.close_mcp_session(session_id)

Explicitly terminate a stateful MCP Streamable HTTP session.

This sends DELETE to the configured MCP endpoint with the Mcp-Session-Id header. When session_id is omitted, the SDK-managed current session is closed. It is only valid for MCP agents using mcp_transport="streamable_http".

async def comply_test_controller(self,
request: ComplyTestControllerRequest) ‑> TaskResult[ComplyTestControllerResponse]
Expand source code
async def comply_test_controller(
    self,
    request: ComplyTestControllerRequest,
) -> TaskResult[ComplyTestControllerResponse]:
    """Compliance test controller for sandbox testing.

    Enables sellers to simulate state transitions and delivery data
    in a sandbox environment for compliance testing.

    Args:
        request: Request specifying scenario and parameters.

    Returns:
        TaskResult containing ComplyTestControllerResponse.
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="comply_test_controller",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.comply_test_controller(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="comply_test_controller",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, ComplyTestControllerResponse)

Compliance test controller for sandbox testing.

Enables sellers to simulate state transitions and delivery data in a sandbox environment for compliance testing.

Args
-----=
request
Request specifying scenario and parameters.

Returns -----= TaskResult containing ComplyTestControllerResponse.

async def context_match(self,
request: ContextMatchRequest) ‑> TaskResult[ContextMatchResponseRouterPublisher]
Expand source code
async def context_match(
    self,
    request: ContextMatchRequest,
) -> TaskResult[ContextMatchResponse]:
    """Match ad context to buyer packages.

    Evaluates contextual signals for a publisher placement against the
    buyer's active packages and returns matching offers.

    Args:
        request: Context match request with placement, property, and
            optional artifact refs, context signals, and geo data.

    Returns:
        TaskResult containing ContextMatchResponse with offers.
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True, by_alias=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="context_match",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.context_match(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="context_match",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, ContextMatchResponse)

Match ad context to buyer packages.

Evaluates contextual signals for a publisher placement against the buyer's active packages and returns matching offers.

Args
-----=
request
Context match request with placement, property, and optional artifact refs, context signals, and geo data.

Returns -----= TaskResult containing ContextMatchResponse with offers.

async def control_media_buy(self,
request: ControlMediaBuyRequest) ‑> TaskResult[Union[ControlMediaBuyResponse1, ControlMediaBuyResponse2, ControlMediaBuyResponse3]]
Expand source code
async def control_media_buy(
    self, request: ControlMediaBuyRequest
) -> TaskResult[ControlMediaBuyResponse]:
    """Apply lifecycle controls to an existing media buy."""
    return cast(
        TaskResult[ControlMediaBuyResponse],
        await self._execute_typed_task("control_media_buy", request, ControlMediaBuyResponse),
    )

Apply lifecycle controls to an existing media buy.

async def create_collection_list(self, request: CreateCollectionListRequest) ‑> TaskResult[CreateCollectionListResponse]
Expand source code
async def create_collection_list(
    self,
    request: CreateCollectionListRequest,
) -> TaskResult[CreateCollectionListResponse]:
    """Create a collection list for governance filtering.

    Collection lists define dynamic sets of collections (properties, segments, etc.)
    that can be referenced by authorization rules and audience scoping.

    Args:
        request: Request parameters for creating the collection list

    Returns:
        TaskResult containing CreateCollectionListResponse with list_id
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="create_collection_list",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.create_collection_list(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="create_collection_list",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, CreateCollectionListResponse)

Create a collection list for governance filtering.

Collection lists define dynamic sets of collections (properties, segments, etc.) that can be referenced by authorization rules and audience scoping.

Args
-----=
request
Request parameters for creating the collection list

Returns -----= TaskResult containing CreateCollectionListResponse with list_id

async def create_content_standards(self, request: CreateContentStandardsRequest) ‑> TaskResult[CreateContentStandardsResponse]
Expand source code
async def create_content_standards(
    self,
    request: CreateContentStandardsRequest,
) -> TaskResult[CreateContentStandardsResponse]:
    """
    Create a new content standards configuration.

    Defines acceptable content contexts for ad placement using natural
    language policy and optional calibration exemplars.

    Args:
        request: Request parameters including policy and scope

    Returns:
        TaskResult containing CreateContentStandardsResponse with standards_id
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="create_content_standards",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.create_content_standards(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="create_content_standards",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, CreateContentStandardsResponse)

Create a new content standards configuration.

Defines acceptable content contexts for ad placement using natural language policy and optional calibration exemplars.

Args
-----=
request
Request parameters including policy and scope

Returns -----= TaskResult containing CreateContentStandardsResponse with standards_id

async def create_media_buy(self,
request: CreateMediaBuyRequest) ‑> TaskResult[Union[CreateMediaBuyResponse1CreateMediaBuyResponse2CreateMediaBuyResponse3]]
Expand source code
async def create_media_buy(
    self,
    request: CreateMediaBuyRequest,
) -> TaskResult[CreateMediaBuyResponse]:
    """
    Create a new media buy reservation.

    Requests the agent to reserve inventory for a campaign. The agent returns a
    media_buy_id that tracks this reservation and can be used for updates.

    Args:
        request: Media buy creation parameters including:
            - brand: Brand reference; resolved from brand.json or the registry at execution
            - packages: List of package requests specifying desired inventory
            - publisher_properties: Target properties for ad placement
            - budget: Optional budget constraints
            - start_date/end_date: Campaign flight dates

    Returns:
        TaskResult containing CreateMediaBuyResponse with:
            - media_buy_id: Unique identifier for this reservation
            - status: Current state of the media buy
            - packages: Confirmed package details
            - Additional platform-specific metadata

    Example:
        >>> from adcp import ADCPClient, CreateMediaBuyRequest, BrandReference
        >>> client = ADCPClient(agent_config)
        >>> request = CreateMediaBuyRequest(
        ...     brand=BrandReference(domain="acme.com"),
        ...     packages=[package_request],
        ...     publisher_properties=properties,
        ... )
        >>> result = await client.create_media_buy(request)
        >>> if result.success:
        ...     media_buy_id = result.data.media_buy_id
    """
    dialect = self._creative_dialect(request, legacy_projection_available=True)
    operation_id = create_operation_id()
    params = self._prepare_creative_params(request)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="create_media_buy",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.create_media_buy(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="create_media_buy",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    if dialect is CreativeDialect.LEGACY:
        return cast(
            TaskResult[CreateMediaBuyResponse],
            self._canonicalize_lifecycle_result(
                raw_result,
                legacy_type=LegacyCreateMediaBuyResponse,
                canonical_type=CreateMediaBuyResponse,
            ),
        )
    return self.adapter._parse_response(raw_result, CreateMediaBuyResponse)

Create a new media buy reservation.

Requests the agent to reserve inventory for a campaign. The agent returns a media_buy_id that tracks this reservation and can be used for updates.

Args
-----=
request
Media buy creation parameters including: - brand: Brand reference; resolved from brand.json or the registry at execution - packages: List of package requests specifying desired inventory - publisher_properties: Target properties for ad placement - budget: Optional budget constraints - start_date/end_date: Campaign flight dates

Returns -----= TaskResult containing CreateMediaBuyResponse with: - media_buy_id: Unique identifier for this reservation - status: Current state of the media buy - packages: Confirmed package details - Additional platform-specific metadata

Example -----=

>>> from adcp import ADCPClient, CreateMediaBuyRequest, BrandReference
>>> client = ADCPClient(agent_config)
>>> request = CreateMediaBuyRequest(
...     brand=BrandReference(domain="acme.com"),
...     packages=[package_request],
...     publisher_properties=properties,
... )
>>> result = await client.create_media_buy(request)
>>> if result.success:
...     media_buy_id = result.data.media_buy_id
async def create_media_buy_legacy(self,
request: CreateMediaBuyRequest) ‑> TaskResult[Union[CreateMediaBuyResponse1CreateMediaBuyResponse2CreateMediaBuyResponse3]]
Expand source code
async def create_media_buy_legacy(
    self, request: LegacyCreateMediaBuyRequest
) -> TaskResult[LegacyCreateMediaBuyResponse]:
    """Execute create_media_buy without the canonical application boundary."""

    self._warn_legacy_creative_api("create_media_buy_legacy")
    raw = await self.adapter.create_media_buy(
        request.model_dump(mode="json", exclude_none=True)
    )
    return self.adapter._parse_response(raw, LegacyCreateMediaBuyResponse)

Execute create_media_buy without the canonical application boundary.

async def create_property_list(self, request: CreatePropertyListRequest) ‑> TaskResult[CreatePropertyListResponse]
Expand source code
async def create_property_list(
    self,
    request: CreatePropertyListRequest,
) -> TaskResult[CreatePropertyListResponse]:
    """
    Create a property list for governance filtering.

    Property lists define dynamic sets of properties based on filters,
    brand manifests, and feature requirements.

    Args:
        request: Request parameters for creating the property list

    Returns:
        TaskResult containing CreatePropertyListResponse with list_id
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="create_property_list",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.create_property_list(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="create_property_list",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, CreatePropertyListResponse)

Create a property list for governance filtering.

Property lists define dynamic sets of properties based on filters, brand manifests, and feature requirements.

Args
-----=
request
Request parameters for creating the property list

Returns -----= TaskResult containing CreatePropertyListResponse with list_id

async def decline_proposals(self,
request: DeclineProposalsRequest) ‑> TaskResult[Union[DeclineProposalsResponse1, DeclineProposalsResponse2]]
Expand source code
async def decline_proposals(
    self, request: DeclineProposalsRequest
) -> TaskResult[DeclineProposalsResponse]:
    """Decline one or more seller proposals."""
    return cast(
        TaskResult[DeclineProposalsResponse],
        await self._execute_typed_task("decline_proposals", request, DeclineProposalsResponse),
    )

Decline one or more seller proposals.

async def delete_collection_list(self, request: DeleteCollectionListRequest) ‑> TaskResult[DeleteCollectionListResponse]
Expand source code
async def delete_collection_list(
    self,
    request: DeleteCollectionListRequest,
) -> TaskResult[DeleteCollectionListResponse]:
    """Delete a collection list.

    Args:
        request: Request parameters with list_id

    Returns:
        TaskResult containing DeleteCollectionListResponse
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="delete_collection_list",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.delete_collection_list(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="delete_collection_list",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, DeleteCollectionListResponse)

Delete a collection list.

Args
-----=
request
Request parameters with list_id

Returns -----= TaskResult containing DeleteCollectionListResponse

async def delete_property_list(self, request: DeletePropertyListRequest) ‑> TaskResult[DeletePropertyListResponse]
Expand source code
async def delete_property_list(
    self,
    request: DeletePropertyListRequest,
) -> TaskResult[DeletePropertyListResponse]:
    """
    Delete a property list.

    Removes a property list. Any active subscriptions to this list
    will be terminated.

    Args:
        request: Request parameters with list_id

    Returns:
        TaskResult containing DeletePropertyListResponse
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="delete_property_list",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.delete_property_list(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="delete_property_list",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, DeletePropertyListResponse)

Delete a property list.

Removes a property list. Any active subscriptions to this list will be terminated.

Args
-----=
request
Request parameters with list_id

Returns -----= TaskResult containing DeletePropertyListResponse

async def execute_task(self, task_name: str, request: BaseModel) ‑> TaskResult[Any]
Expand source code
async def execute_task(self, task_name: str, request: BaseModel) -> TaskResult[Any]:
    """Execute a standard task through the canonical primary API map."""

    legacy_only_tasks = {
        "build_creative",
        "list_creative_formats",
        "preview_creative",
    }
    if task_name in legacy_only_tasks:
        raise ValueError(
            f"{task_name} is legacy-only; use execute_task_legacy() or the "
            "corresponding *_legacy method"
        )
    serialized = request.model_dump(mode="json", exclude_none=True)
    if strip_legacy_creative_identity(serialized) != serialized:
        raise ValueError(
            f"{task_name} request contains legacy creative identity; generic execute_task "
            "is canonical-only"
        )
    method = getattr(self, task_name, None)
    if method is None or not callable(method) or task_name.endswith("_legacy"):
        raise ValueError(f"Unknown canonical AdCP task: {task_name}")
    return cast(TaskResult[Any], await method(request))

Execute a standard task through the canonical primary API map.

async def execute_task_legacy(self, task_name: str, request: BaseModel) ‑> TaskResult[Any]
Expand source code
async def execute_task_legacy(self, task_name: str, request: BaseModel) -> TaskResult[Any]:
    """Execute an explicitly raw creative task for migration tooling."""

    methods: dict[str, Callable[[Any], Any]] = {
        "build_creative": self.build_creative_legacy,
        "create_media_buy": self.create_media_buy_legacy,
        "get_creative_delivery": self.get_creative_delivery_legacy,
        "get_media_buy_delivery": self.get_media_buy_delivery_legacy,
        "get_media_buys": self.get_media_buys_legacy,
        "get_products": self.get_products_legacy,
        "list_creative_formats": self.list_creative_formats_legacy,
        "list_creatives": self.list_creatives_legacy,
        "preview_creative": self.preview_creative_legacy,
        "sync_creatives": self.sync_creatives_legacy,
        "update_media_buy": self.update_media_buy_legacy,
    }
    method = methods.get(task_name)
    if method is None:
        raise ValueError(
            f"No explicit legacy task adapter for {task_name!r}; "
            "use the raw protocol adapter for conformance tooling"
        )
    return cast(TaskResult[Any], await method(request))

Execute an explicitly raw creative task for migration tooling.

async def fetch_capabilities(self) ‑> adcp.types.generated_poc.protocol.get_adcp_capabilities_response.GetAdcpCapabilitiesResponse
Expand source code
async def fetch_capabilities(self) -> GetAdcpCapabilitiesResponse:
    """Fetch capabilities, using cache if still valid.

    Returns:
        The seller's capabilities response.
    """
    if self._capabilities is not None and self._capabilities_fetched_at is not None:
        elapsed = time.monotonic() - self._capabilities_fetched_at
        if elapsed < self.capabilities_ttl:
            return self._capabilities

    return await self.refresh_capabilities()

Fetch capabilities, using cache if still valid.

Returns -----= The seller's capabilities response.

async def get_account_financials(self,
request: GetAccountFinancialsRequest) ‑> TaskResult[Union[GetAccountFinancialsResponse1GetAccountFinancialsResponse2]]
Expand source code
async def get_account_financials(
    self,
    request: GetAccountFinancialsRequest,
) -> TaskResult[GetAccountFinancialsResponse]:
    """
    Get Account Financials.

    Args:
        request: Request parameters

    Returns:
        TaskResult containing GetAccountFinancialsResponse
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="get_account_financials",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.get_account_financials(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="get_account_financials",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, GetAccountFinancialsResponse)

Get Account Financials.

Args
-----=
request
Request parameters

Returns -----= TaskResult containing GetAccountFinancialsResponse

async def get_adcp_capabilities(self, request: GetAdcpCapabilitiesRequest) ‑> TaskResult[GetAdcpCapabilitiesResponse]
Expand source code
async def get_adcp_capabilities(
    self,
    request: GetAdcpCapabilitiesRequest,
) -> TaskResult[GetAdcpCapabilitiesResponse]:
    """
    Get AdCP capabilities from the agent.

    Queries the agent's supported AdCP features, protocol versions, and
    domain-specific capabilities (media_buy, signals, sponsored_intelligence).

    Args:
        request: Request parameters including optional protocol filters

    Returns:
        TaskResult containing GetAdcpCapabilitiesResponse with:
            - adcp: Core protocol version information
            - supported_protocols: List of supported domain protocols
            - media_buy: Media buy capabilities (if supported)
            - sponsored_intelligence: SI capabilities (if supported)
            - signals: Signals capabilities (if supported)
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="get_adcp_capabilities",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.get_adcp_capabilities(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="get_adcp_capabilities",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, GetAdcpCapabilitiesResponse)

Get AdCP capabilities from the agent.

Queries the agent's supported AdCP features, protocol versions, and domain-specific capabilities (media_buy, signals, sponsored_intelligence).

Args
-----=
request
Request parameters including optional protocol filters

Returns -----= TaskResult containing GetAdcpCapabilitiesResponse with: - adcp: Core protocol version information - supported_protocols: List of supported domain protocols - media_buy: Media buy capabilities (if supported) - sponsored_intelligence: SI capabilities (if supported) - signals: Signals capabilities (if supported)

def get_adcp_version(self) ‑> str
Expand source code
def get_adcp_version(self) -> str:
    """Return the AdCP protocol release this client is pinned to.

    Resolved at construction from the ``adcp_version`` kwarg, with
    fallback to the SDK's compile-time pin (``ADCP_VERSION``
    packaged with the wheel) when the caller didn't pin
    explicitly. Same value across the client's lifetime — the pin
    is per-instance, not per-call.

    See ``__init__``'s ``adcp_version`` parameter for the full
    semantics, including the cross-major fence and dialect selection.
    """
    return self._adcp_version

Return the AdCP protocol release this client is pinned to.

Resolved at construction from the adcp_version kwarg, with fallback to the SDK's compile-time pin (ADCP_VERSION packaged with the wheel) when the caller didn't pin explicitly. Same value across the client's lifetime — the pin is per-instance, not per-call.

See __init__'s adcp_version parameter for the full semantics, including the cross-major fence and dialect selection.

async def get_brand_identity(self,
request: GetBrandIdentityRequest) ‑> TaskResult[Union[GetBrandIdentityResponse1GetBrandIdentityResponse2]]
Expand source code
async def get_brand_identity(
    self,
    request: GetBrandIdentityRequest,
) -> TaskResult[GetBrandIdentityResponse]:
    """Get brand identity information.

    Retrieves brand identity data including logos, colors, fonts,
    voice synthesis config, and rights availability.

    Args:
        request: Request with brand_id and optional fields filter.

    Returns:
        TaskResult containing GetBrandIdentityResponse.
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="get_brand_identity",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.get_brand_identity(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="get_brand_identity",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, GetBrandIdentityResponse)

Get brand identity information.

Retrieves brand identity data including logos, colors, fonts, voice synthesis config, and rights availability.

Args
-----=
request
Request with brand_id and optional fields filter.

Returns -----= TaskResult containing GetBrandIdentityResponse.

async def get_collection_list(self, request: GetCollectionListRequest) ‑> TaskResult[GetCollectionListResponse]
Expand source code
async def get_collection_list(
    self,
    request: GetCollectionListRequest,
) -> TaskResult[GetCollectionListResponse]:
    """Get a collection list with optional resolution.

    When resolve=true, returns the resolved members of the collection list.

    Args:
        request: Request parameters including list_id and resolve flag

    Returns:
        TaskResult containing GetCollectionListResponse
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="get_collection_list",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.get_collection_list(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="get_collection_list",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, GetCollectionListResponse)

Get a collection list with optional resolution.

When resolve=true, returns the resolved members of the collection list.

Args
-----=
request
Request parameters including list_id and resolve flag

Returns -----= TaskResult containing GetCollectionListResponse

async def get_content_standards(self, request: GetContentStandardsRequest) ‑> TaskResult[Union[GetContentStandardsResponse1GetContentStandardsResponse2]]
Expand source code
async def get_content_standards(
    self,
    request: GetContentStandardsRequest,
) -> TaskResult[GetContentStandardsResponse]:
    """
    Get a content standards configuration by ID.

    Args:
        request: Request parameters including standards_id

    Returns:
        TaskResult containing GetContentStandardsResponse
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="get_content_standards",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.get_content_standards(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="get_content_standards",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, GetContentStandardsResponse)

Get a content standards configuration by ID.

Args
-----=
request
Request parameters including standards_id

Returns -----= TaskResult containing GetContentStandardsResponse

async def get_creative_delivery(self,
request: GetCreativeDeliveryRequest) ‑> TaskResult[GetCreativeDeliveryResponse]
Expand source code
async def get_creative_delivery(
    self,
    request: GetCreativeDeliveryRequest,
) -> TaskResult[GetCreativeDeliveryResponse]:
    """
    Get Creative Delivery.

    Args:
        request: Request parameters

    Returns:
        TaskResult containing GetCreativeDeliveryResponse
    """
    self._creative_dialect(request, legacy_projection_available=True)
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="get_creative_delivery",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.get_creative_delivery(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="get_creative_delivery",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    if (
        self._creative_dialect(request, legacy_projection_available=True)
        is CreativeDialect.CANONICAL
    ):
        return self.adapter._parse_response(raw_result, GetCreativeDeliveryResponse)
    return cast(
        TaskResult[GetCreativeDeliveryResponse],
        self._canonicalize_format_read_result(
            raw_result,
            legacy_type=LegacyGetCreativeDeliveryResponse,
            canonical_type=GetCreativeDeliveryResponse,
            collection="creatives",
            require_format=False,
        ),
    )

Get Creative Delivery.

Args
-----=
request
Request parameters

Returns -----= TaskResult containing GetCreativeDeliveryResponse

async def get_creative_delivery_legacy(self,
request: GetCreativeDeliveryRequest) ‑> TaskResult[GetCreativeDeliveryResponse]
Expand source code
async def get_creative_delivery_legacy(
    self, request: GetCreativeDeliveryRequest
) -> TaskResult[LegacyGetCreativeDeliveryResponse]:
    """Return raw creative delivery carrying legacy format identity."""

    self._warn_legacy_creative_api("get_creative_delivery_legacy")
    raw = await self.adapter.get_creative_delivery(
        request.model_dump(mode="json", exclude_none=True)
    )
    return self.adapter._parse_response(raw, LegacyGetCreativeDeliveryResponse)

Return raw creative delivery carrying legacy format identity.

async def get_creative_features(self,
request: GetCreativeFeaturesRequest) ‑> TaskResult[Union[GetCreativeFeaturesResponse1GetCreativeFeaturesResponse2]]
Expand source code
async def get_creative_features(
    self,
    request: GetCreativeFeaturesRequest,
) -> TaskResult[GetCreativeFeaturesResponse]:
    """Evaluate governance features for a creative manifest."""
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="get_creative_features",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.get_creative_features(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="get_creative_features",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, GetCreativeFeaturesResponse)

Evaluate governance features for a creative manifest.

async def get_info(self) ‑> dict[str, typing.Any]
Expand source code
async def get_info(self) -> dict[str, Any]:
    """
    Get agent information including AdCP extension metadata.

    Returns agent card information including:
    - Agent name, description, version
    - Protocol type (mcp or a2a)
    - AdCP version (from extensions.adcp.adcp_version)
    - Supported protocols (from extensions.adcp.protocols_supported)
    - Available tools/skills

    Returns:
        Dictionary with agent metadata
    """
    return await self.adapter.get_agent_info()

Get agent information including AdCP extension metadata.

Returns agent card information including: - Agent name, description, version - Protocol type (mcp or a2a) - AdCP version (from extensions.adcp.adcp_version) - Supported protocols (from extensions.adcp.protocols_supported) - Available tools/skills

Returns -----= Dictionary with agent metadata

async def get_media_buy_artifacts(self, request: GetMediaBuyArtifactsRequest) ‑> TaskResult[Union[GetMediaBuyArtifactsResponse1GetMediaBuyArtifactsResponse2]]
Expand source code
async def get_media_buy_artifacts(
    self,
    request: GetMediaBuyArtifactsRequest,
) -> TaskResult[GetMediaBuyArtifactsResponse]:
    """
    Get artifacts associated with a media buy.

    Retrieves content artifacts where ads were delivered for a media buy.

    Args:
        request: Request parameters including media_buy_id

    Returns:
        TaskResult containing GetMediaBuyArtifactsResponse
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="get_media_buy_artifacts",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.get_media_buy_artifacts(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="get_media_buy_artifacts",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, GetMediaBuyArtifactsResponse)

Get artifacts associated with a media buy.

Retrieves content artifacts where ads were delivered for a media buy.

Args
-----=
request
Request parameters including media_buy_id

Returns -----= TaskResult containing GetMediaBuyArtifactsResponse

async def get_media_buy_delivery(self,
request: GetMediaBuyDeliveryRequest) ‑> TaskResult[GetMediaBuyDeliveryResponse]
Expand source code
async def get_media_buy_delivery(
    self,
    request: GetMediaBuyDeliveryRequest,
) -> TaskResult[GetMediaBuyDeliveryResponse]:
    """
    Get Media Buy Delivery.

    Args:
        request: Request parameters

    Returns:
        TaskResult containing GetMediaBuyDeliveryResponse
    """
    dialect = self._creative_dialect(request, legacy_projection_available=True)
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="get_media_buy_delivery",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.get_media_buy_delivery(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="get_media_buy_delivery",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    if dialect is CreativeDialect.LEGACY:
        return cast(
            TaskResult[GetMediaBuyDeliveryResponse],
            self._canonicalize_lifecycle_result(
                raw_result,
                legacy_type=LegacyGetMediaBuyDeliveryResponse,
                canonical_type=GetMediaBuyDeliveryResponse,
            ),
        )
    return self.adapter._parse_response(raw_result, GetMediaBuyDeliveryResponse)

Get Media Buy Delivery.

Args
-----=
request
Request parameters

Returns -----= TaskResult containing GetMediaBuyDeliveryResponse

async def get_media_buy_delivery_legacy(self,
request: GetMediaBuyDeliveryRequest) ‑> TaskResult[GetMediaBuyDeliveryResponse]
Expand source code
async def get_media_buy_delivery_legacy(
    self, request: GetMediaBuyDeliveryRequest
) -> TaskResult[LegacyGetMediaBuyDeliveryResponse]:
    """Return raw media-buy delivery carrying legacy format identity."""

    self._warn_legacy_creative_api("get_media_buy_delivery_legacy")
    raw = await self.adapter.get_media_buy_delivery(
        request.model_dump(mode="json", exclude_none=True)
    )
    return self.adapter._parse_response(raw, LegacyGetMediaBuyDeliveryResponse)

Return raw media-buy delivery carrying legacy format identity.

async def get_media_buys(self,
request: GetMediaBuysRequest) ‑> TaskResult[GetMediaBuysResponse]
Expand source code
async def get_media_buys(
    self,
    request: GetMediaBuysRequest,
) -> TaskResult[GetMediaBuysResponse]:
    """
    Get Media Buys.

    Args:
        request: Request parameters

    Returns:
        TaskResult containing GetMediaBuysResponse
    """
    dialect = self._creative_dialect(request, legacy_projection_available=True)
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)
    if params.get("include_webhook_activity") is False:
        params.pop("include_webhook_activity")
    if params.get("webhook_activity_limit") == 50:
        params.pop("webhook_activity_limit")

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="get_media_buys",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.get_media_buys(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="get_media_buys",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    if dialect is CreativeDialect.LEGACY:
        return cast(
            TaskResult[GetMediaBuysResponse],
            self._canonicalize_lifecycle_result(
                raw_result,
                legacy_type=LegacyGetMediaBuysResponse,
                canonical_type=GetMediaBuysResponse,
            ),
        )
    return self.adapter._parse_response(raw_result, GetMediaBuysResponse)

Get Media Buys.

Args
-----=
request
Request parameters

Returns -----= TaskResult containing GetMediaBuysResponse

async def get_media_buys_legacy(self,
request: GetMediaBuysRequest) ‑> TaskResult[GetMediaBuysResponse]
Expand source code
async def get_media_buys_legacy(
    self, request: GetMediaBuysRequest
) -> TaskResult[LegacyGetMediaBuysResponse]:
    """Return raw media-buy rows carrying legacy format identity."""

    self._warn_legacy_creative_api("get_media_buys_legacy")
    raw = await self.adapter.get_media_buys(request.model_dump(mode="json", exclude_none=True))
    return self.adapter._parse_response(raw, LegacyGetMediaBuysResponse)

Return raw media-buy rows carrying legacy format identity.

async def get_plan_audit_logs(self,
request: GetPlanAuditLogsRequest) ‑> TaskResult[GetPlanAuditLogsResponse]
Expand source code
async def get_plan_audit_logs(
    self,
    request: GetPlanAuditLogsRequest,
) -> TaskResult[GetPlanAuditLogsResponse]:
    """Retrieve governance state and audit logs for one or more plans."""
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="get_plan_audit_logs",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.get_plan_audit_logs(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="get_plan_audit_logs",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, GetPlanAuditLogsResponse)

Retrieve governance state and audit logs for one or more plans.

async def get_products(self,
request: GetProductsRequest,
fetch_previews: bool = False,
preview_output_format: str = 'url',
creative_agent_client: ADCPClient | None = None) ‑> TaskResult[GetProductsResponse]
Expand source code
async def get_products(
    self,
    request: GetProductsRequest,
    fetch_previews: bool = False,
    preview_output_format: str = "url",
    creative_agent_client: ADCPClient | None = None,
) -> TaskResult[GetProductsResponse]:
    """
    Get advertising products.

    Args:
        request: Request parameters
        fetch_previews: If True, generate preview URLs for each product's formats
            (uses batch API for 5-10x performance improvement)
        preview_output_format: "url" for iframe URLs (default), "html" for direct
            embedding (2-3x faster, no iframe overhead)
        creative_agent_client: Client for creative agent (required if
            fetch_previews=True)

    Returns:
        TaskResult containing GetProductsResponse with optional preview URLs in metadata

    Raises:
        ValueError: If fetch_previews=True but creative_agent_client is not provided
    """
    if fetch_previews and not creative_agent_client:
        raise ValueError("creative_agent_client is required when fetch_previews=True")

    self._creative_dialect(request, legacy_projection_available=True)
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="get_products",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.get_products(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="get_products",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    result = self._canonicalize_get_products_result(raw_result)

    if (
        fetch_previews
        and result.success
        and result.data
        and result.data.products
        and creative_agent_client
    ):
        from adcp.utils.preview_cache import add_preview_urls_to_products

        products_with_previews = await add_preview_urls_to_products(
            result.data.products,
            creative_agent_client,
            use_batch=True,
            output_format=preview_output_format,
        )
        result.metadata = result.metadata or {}
        result.metadata["products_with_previews"] = products_with_previews

    return result

Get advertising products.

Args
-----=
request
Request parameters
fetch_previews
If True, generate preview URLs for each product's formats (uses batch API for 5-10x performance improvement)
preview_output_format
"url" for iframe URLs (default), "html" for direct embedding (2-3x faster, no iframe overhead)
creative_agent_client
Client for creative agent (required if fetch_previews=True)

Returns -----= TaskResult containing GetProductsResponse with optional preview URLs in metadata

Raises
-----=
ValueError
If fetch_previews=True but creative_agent_client is not provided
async def get_products_legacy(self,
request: GetProductsRequest) ‑> TaskResult[GetProductsResponse]
Expand source code
async def get_products_legacy(
    self,
    request: LegacyGetProductsRequest,
) -> TaskResult[LegacyGetProductsResponse]:
    """Return the raw AdCP 3.x product wire shape for migration tooling."""

    self._warn_legacy_creative_api("get_products_legacy")
    raw_result = await self.adapter.get_products(request.model_dump(mode="json"))
    return self.adapter._parse_response(raw_result, LegacyGetProductsResponse)

Return the raw AdCP 3.x product wire shape for migration tooling.

async def get_property_list(self, request: GetPropertyListRequest) ‑> TaskResult[GetPropertyListResponse]
Expand source code
async def get_property_list(
    self,
    request: GetPropertyListRequest,
) -> TaskResult[GetPropertyListResponse]:
    """
    Get a property list with optional resolution.

    When resolve=true, returns the list of resolved property identifiers.
    Use this to get the actual properties that match the list's filters.

    Args:
        request: Request parameters including list_id and resolve flag

    Returns:
        TaskResult containing GetPropertyListResponse with identifiers
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="get_property_list",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.get_property_list(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="get_property_list",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, GetPropertyListResponse)

Get a property list with optional resolution.

When resolve=true, returns the list of resolved property identifiers. Use this to get the actual properties that match the list's filters.

Args
-----=
request
Request parameters including list_id and resolve flag

Returns -----= TaskResult containing GetPropertyListResponse with identifiers

async def get_rights(self,
request: GetRightsRequest) ‑> TaskResult[Union[GetRightsResponse1GetRightsResponse2]]
Expand source code
async def get_rights(
    self,
    request: GetRightsRequest,
) -> TaskResult[GetRightsResponse]:
    """Get available rights for licensing.

    Searches for rights offerings using natural language query and
    filters by type, uses, countries, and buyer compatibility.

    Args:
        request: Request with query, uses, and optional filters.

    Returns:
        TaskResult containing GetRightsResponse with matched rights.
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="get_rights",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.get_rights(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="get_rights",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, GetRightsResponse)

Get available rights for licensing.

Searches for rights offerings using natural language query and filters by type, uses, countries, and buyer compatibility.

Args
-----=
request
Request with query, uses, and optional filters.

Returns -----= TaskResult containing GetRightsResponse with matched rights.

def get_server_version(self) ‑> str | None
Expand source code
def get_server_version(self) -> str | None:
    """Return the seller's AdCP wire-shape version, or ``None``.

    ``None`` means the SDK is assuming a current-major seller
    (the default). Returns a release-precision string
    (``"3.0"``, ``"3.1"``, ``"2.5"``) when the adopter pinned
    via the ``server_version`` constructor arg or — once the
    agent-card probe lands — when the SDK detected the seller's
    version from its agent-card.

    See ``__init__``'s ``server_version`` parameter for negotiated
    canonical/legacy creative behavior.
    """
    return self._server_version

Return the seller's AdCP wire-shape version, or None.

None means the SDK is assuming a current-major seller (the default). Returns a release-precision string ("3.0", "3.1", "2.5") when the adopter pinned via the server_version constructor arg or — once the agent-card probe lands — when the SDK detected the seller's version from its agent-card.

See __init__'s server_version parameter for negotiated canonical/legacy creative behavior.

async def get_signals(self,
request: GetSignalsRequest) ‑> TaskResult[GetSignalsResponse]
Expand source code
async def get_signals(
    self,
    request: GetSignalsRequest,
) -> TaskResult[GetSignalsResponse]:
    """
    Get Signals.

    Args:
        request: Request parameters

    Returns:
        TaskResult containing GetSignalsResponse
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="get_signals",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.get_signals(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="get_signals",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, GetSignalsResponse)

Get Signals.

Args
-----=
request
Request parameters

Returns -----= TaskResult containing GetSignalsResponse

async def get_task_status(self,
request: GetTaskStatusRequest) ‑> TaskResult[GetTaskStatusResponse]
Expand source code
async def get_task_status(
    self,
    request: GetTaskStatusRequest,
) -> TaskResult[GetTaskStatusResponse]:
    """
    Get Task Status.

    Args:
        request: Request parameters

    Returns:
        TaskResult containing GetTaskStatusResponse
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="get_task_status",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.get_task_status(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="get_task_status",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, GetTaskStatusResponse)

Get Task Status.

Args
-----=
request
Request parameters

Returns -----= TaskResult containing GetTaskStatusResponse

def get_webhook_url(self, task_type: str, operation_id: str) ‑> str
Expand source code
def get_webhook_url(self, task_type: str, operation_id: str) -> str:
    """Generate webhook URL for a task."""
    if not self.webhook_url_template:
        raise ValueError("webhook_url_template not configured")

    return self.webhook_url_template.format(
        agent_id=self.agent_config.id,
        task_type=task_type,
        operation_id=operation_id,
    )

Generate webhook URL for a task.

async def handle_webhook(self,
payload: dict[str, Any] | Task | TaskStatusUpdateEvent,
task_type: str,
operation_id: str,
signature: str | None = None,
timestamp: str | None = None,
raw_body: bytes | str | None = None) ‑> TaskResult[AdcpAsyncResponseData]
Expand source code
async def handle_webhook(
    self,
    payload: dict[str, Any] | Task | TaskStatusUpdateEvent,
    task_type: str,
    operation_id: str,
    signature: str | None = None,
    timestamp: str | None = None,
    raw_body: bytes | str | None = None,
) -> TaskResult[AdcpAsyncResponseData]:
    """
    Handle incoming webhook and return typed result.

    This method provides a unified interface for handling webhooks from both
    MCP and A2A protocols:

    - MCP Webhooks: HTTP POST with dict payload; the deprecated HMAC fallback
      requires a signature, timestamp, and raw body
    - A2A Webhooks: Task or TaskStatusUpdateEvent objects based on status

    The method automatically detects the protocol type and routes to the
    appropriate handler. Both protocols return a consistent TaskResult
    structure with typed AdCP response data.

    Args:
        payload: Webhook payload - one of:
            - dict[str, Any]: MCP webhook payload from HTTP POST
            - Task: A2A webhook for terminated statuses (completed, failed)
            - TaskStatusUpdateEvent: A2A webhook for intermediate statuses
              (working, input-required, submitted)
        task_type: Task type from application routing for A2A callbacks. For
            MCP callbacks, the validated payload's authenticated ``task_type``
            controls parsing and activity correlation.
        operation_id: Operation identifier from application routing. For MCP
            callbacks, the authenticated payload value controls correlation
            when present; this argument is a compatibility fallback for old
            payloads that omit it.
        signature: HMAC-SHA256 signature from X-AdCP-Signature. Required when
            ``webhook_secret`` configures the deprecated HMAC fallback and
            ignored for A2A callbacks.
        timestamp: Unix timestamp from X-AdCP-Timestamp. Required with the
            deprecated HMAC fallback and ignored for A2A callbacks.
        raw_body: Raw HTTP request body captured before JSON parsing. Required
            with the deprecated HMAC fallback and ignored for A2A callbacks.

    Returns:
        TaskResult with parsed task-specific response data. The structure
        is identical regardless of protocol.

    Raises:
        ADCPWebhookSignatureError: If MCP signature verification fails
        ValidationError: If MCP payload doesn't match WebhookPayload schema

    Note:
        AdCP-conformant public MCP endpoints should use
        :class:`adcp.webhooks.WebhookReceiver`, which verifies RFC 9421,
        deduplicates retries, and parses the authenticated body. This method's
        HMAC mode exists only for explicitly selected legacy registrations.

    Examples:
        MCP webhook (HTTP endpoint):
        >>> @app.post("/webhook/{task_type}/{agent_id}/{operation_id}")
        >>> async def webhook_handler(task_type: str, operation_id: str, request: Request):
        >>>     raw_body = await request.body()
        >>>     payload = json.loads(raw_body)
        >>>     signature = request.headers.get("X-AdCP-Signature")
        >>>     timestamp = request.headers.get("X-AdCP-Timestamp")
        >>>     result = await client.handle_webhook(
        >>>         payload, task_type, operation_id, signature, timestamp,
        >>>         raw_body=raw_body,
        >>>     )
        >>>     if result.success:
        >>>         print(f"Task completed: {result.data}")

        A2A webhook with Task (terminated status):
        >>> async def on_task_completed(task: Task):
        >>>     # Extract task_type and operation_id from your app's task tracking
        >>>     task_type = your_task_registry.get_type(task.id)
        >>>     operation_id = your_task_registry.get_operation_id(task.id)
        >>>     result = await client.handle_webhook(
        >>>         task, task_type, operation_id
        >>>     )
        >>>     if result.success:
        >>>         print(f"Task completed: {result.data}")

        A2A webhook with TaskStatusUpdateEvent (intermediate status):
        >>> async def on_task_update(event: TaskStatusUpdateEvent):
        >>>     # Extract task_type and operation_id from your app's task tracking
        >>>     task_type = your_task_registry.get_type(event.task_id)
        >>>     operation_id = your_task_registry.get_operation_id(event.task_id)
        >>>     result = await client.handle_webhook(
        >>>         event, task_type, operation_id
        >>>     )
        >>>     if result.status == GeneratedTaskStatus.working:
        >>>         print(f"Task still working: {result.metadata.get('message')}")
    """
    if (
        isinstance(payload, (Task, TaskStatusUpdateEvent))
        and task_type in _LEGACY_ONLY_CREATIVE_TASKS
    ):
        raise ValueError(
            f"{task_type} webhook payloads carry legacy creative identity; use "
            "handle_webhook_legacy()"
        )
    result = await self._dispatch_webhook(
        payload,
        task_type,
        operation_id,
        signature,
        timestamp,
        raw_body,
        preserve_legacy_identity=False,
    )
    sanitized = strip_legacy_creative_identity(result.model_dump(mode="python"))
    return TaskResult[AdcpAsyncResponseData].model_validate(sanitized)

Handle incoming webhook and return typed result.

This method provides a unified interface for handling webhooks from both MCP and A2A protocols:

  • MCP Webhooks: HTTP POST with dict payload; the deprecated HMAC fallback requires a signature, timestamp, and raw body
  • A2A Webhooks: Task or TaskStatusUpdateEvent objects based on status

The method automatically detects the protocol type and routes to the appropriate handler. Both protocols return a consistent TaskResult structure with typed AdCP response data.

Args
-----=
payload
Webhook payload - one of: - dict[str, Any]: MCP webhook payload from HTTP POST - Task: A2A webhook for terminated statuses (completed, failed) - TaskStatusUpdateEvent: A2A webhook for intermediate statuses (working, input-required, submitted)
task_type
Task type from application routing for A2A callbacks. For MCP callbacks, the validated payload's authenticated task_type controls parsing and activity correlation.
operation_id
Operation identifier from application routing. For MCP callbacks, the authenticated payload value controls correlation when present; this argument is a compatibility fallback for old payloads that omit it.
signature
HMAC-SHA256 signature from X-AdCP-Signature. Required when webhook_secret configures the deprecated HMAC fallback and ignored for A2A callbacks.
timestamp
Unix timestamp from X-AdCP-Timestamp. Required with the deprecated HMAC fallback and ignored for A2A callbacks.
raw_body
Raw HTTP request body captured before JSON parsing. Required with the deprecated HMAC fallback and ignored for A2A callbacks.

Returns -----= TaskResult with parsed task-specific response data. The structure is identical regardless of protocol.

Raises
-----=
ADCPWebhookSignatureError
If MCP signature verification fails
ValidationError
If MCP payload doesn't match WebhookPayload schema

Note -----= AdCP-conformant public MCP endpoints should use :class:WebhookReceiver, which verifies RFC 9421, deduplicates retries, and parses the authenticated body. This method's HMAC mode exists only for explicitly selected legacy registrations.

Examples -----= MCP webhook (HTTP endpoint):

>>> @app.post("/webhook/{task_type}/{agent_id}/{operation_id}")
>>> async def webhook_handler(task_type: str, operation_id: str, request: Request):
>>>     raw_body = await request.body()
>>>     payload = json.loads(raw_body)
>>>     signature = request.headers.get("X-AdCP-Signature")
>>>     timestamp = request.headers.get("X-AdCP-Timestamp")
>>>     result = await client.handle_webhook(
>>>         payload, task_type, operation_id, signature, timestamp,
>>>         raw_body=raw_body,
>>>     )
>>>     if result.success:
>>>         print(f"Task completed: {result.data}")

A2A webhook with Task (terminated status):

>>> async def on_task_completed(task: Task):
>>>     # Extract task_type and operation_id from your app's task tracking
>>>     task_type = your_task_registry.get_type(task.id)
>>>     operation_id = your_task_registry.get_operation_id(task.id)
>>>     result = await client.handle_webhook(
>>>         task, task_type, operation_id
>>>     )
>>>     if result.success:
>>>         print(f"Task completed: {result.data}")

A2A webhook with TaskStatusUpdateEvent (intermediate status):

>>> async def on_task_update(event: TaskStatusUpdateEvent):
>>>     # Extract task_type and operation_id from your app's task tracking
>>>     task_type = your_task_registry.get_type(event.task_id)
>>>     operation_id = your_task_registry.get_operation_id(event.task_id)
>>>     result = await client.handle_webhook(
>>>         event, task_type, operation_id
>>>     )
>>>     if result.status == GeneratedTaskStatus.working:
>>>         print(f"Task still working: {result.metadata.get('message')}")
async def handle_webhook_legacy(self,
payload: dict[str, Any] | Task | TaskStatusUpdateEvent,
task_type: str,
operation_id: str,
signature: str | None = None,
timestamp: str | None = None,
raw_body: bytes | str | None = None) ‑> TaskResult[AdcpAsyncResponseData]
Expand source code
async def handle_webhook_legacy(
    self,
    payload: dict[str, Any] | Task | TaskStatusUpdateEvent,
    task_type: str,
    operation_id: str,
    signature: str | None = None,
    timestamp: str | None = None,
    raw_body: bytes | str | None = None,
) -> TaskResult[AdcpAsyncResponseData]:
    """Parse a callback for a task whose protocol shape is explicitly legacy-only."""

    if isinstance(payload, (Task, TaskStatusUpdateEvent)) and task_type not in (
        _LEGACY_CREATIVE_TASKS
    ):
        raise ValueError(f"{task_type} is not a legacy-only callback; use handle_webhook()")
    self._warn_legacy_creative_api("handle_webhook_legacy")
    return await self._dispatch_webhook(
        payload,
        task_type,
        operation_id,
        signature,
        timestamp,
        raw_body,
        preserve_legacy_identity=True,
    )

Parse a callback for a task whose protocol shape is explicitly legacy-only.

async def identity_match(self,
request: IdentityMatchRequest) ‑> TaskResult[IdentityMatchResponseRouterPublisher]
Expand source code
async def identity_match(
    self,
    request: IdentityMatchRequest,
) -> TaskResult[IdentityMatchResponse]:
    """Match user identity for package eligibility.

    Evaluates a user identity token against all active packages for
    frequency capping and personalization.

    Args:
        request: Identity match request with user_token, uid_type,
            and package_ids.

    Returns:
        TaskResult containing IdentityMatchResponse with eligible_package_ids.
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True, by_alias=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="identity_match",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.identity_match(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="identity_match",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, IdentityMatchResponse)

Match user identity for package eligibility.

Evaluates a user identity token against all active packages for frequency capping and personalization.

Args
-----=
request
Identity match request with user_token, uid_type, and package_ids.

Returns -----= TaskResult containing IdentityMatchResponse with eligible_package_ids.

async def list_accounts(self,
request: ListAccountsRequest) ‑> TaskResult[ListAccountsResponse]
Expand source code
async def list_accounts(
    self,
    request: ListAccountsRequest,
) -> TaskResult[ListAccountsResponse]:
    """
    List Accounts.

    Args:
        request: Request parameters

    Returns:
        TaskResult containing ListAccountsResponse
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="list_accounts",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.list_accounts(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="list_accounts",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, ListAccountsResponse)

List Accounts.

Args
-----=
request
Request parameters

Returns -----= TaskResult containing ListAccountsResponse

async def list_collection_lists(self, request: ListCollectionListsRequest) ‑> TaskResult[ListCollectionListsResponse]
Expand source code
async def list_collection_lists(
    self,
    request: ListCollectionListsRequest,
) -> TaskResult[ListCollectionListsResponse]:
    """List collection lists owned by a principal.

    Args:
        request: Request parameters with optional filtering

    Returns:
        TaskResult containing ListCollectionListsResponse
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="list_collection_lists",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.list_collection_lists(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="list_collection_lists",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, ListCollectionListsResponse)

List collection lists owned by a principal.

Args
-----=
request
Request parameters with optional filtering

Returns -----= TaskResult containing ListCollectionListsResponse

async def list_content_standards(self, request: ListContentStandardsRequest) ‑> TaskResult[ListContentStandardsResponse]
Expand source code
async def list_content_standards(
    self,
    request: ListContentStandardsRequest,
) -> TaskResult[ListContentStandardsResponse]:
    """
    List content standards configurations.

    Args:
        request: Request parameters including optional filters

    Returns:
        TaskResult containing ListContentStandardsResponse
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="list_content_standards",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.list_content_standards(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="list_content_standards",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, ListContentStandardsResponse)

List content standards configurations.

Args
-----=
request
Request parameters including optional filters

Returns -----= TaskResult containing ListContentStandardsResponse

async def list_creative_formats_legacy(self,
request: ListCreativeFormatsRequest,
fetch_previews: bool = False,
preview_output_format: str = 'url') ‑> TaskResult[ListCreativeFormatsResponse]
Expand source code
async def list_creative_formats_legacy(
    self,
    request: ListCreativeFormatsRequest,
    fetch_previews: bool = False,
    preview_output_format: str = "url",
) -> TaskResult[ListCreativeFormatsResponse]:
    """
    List supported creative formats.

    Args:
        request: Request parameters
        fetch_previews: If True, generate preview URLs for each format using
            sample manifests (uses batch API for 5-10x performance improvement)
        preview_output_format: "url" for iframe URLs (default), "html" for direct
            embedding (2-3x faster, no iframe overhead)

    Returns:
        TaskResult containing ListCreativeFormatsResponse with optional preview URLs in metadata
    """
    self._warn_legacy_creative_api("list_creative_formats_legacy")
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="list_creative_formats",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.list_creative_formats(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="list_creative_formats",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    result: TaskResult[ListCreativeFormatsResponse] = self.adapter._parse_response(
        raw_result, ListCreativeFormatsResponse
    )

    if fetch_previews and result.success and result.data:
        from adcp.utils.preview_cache import add_preview_urls_to_formats

        formats_with_previews = await add_preview_urls_to_formats(
            result.data.formats,
            self,
            use_batch=True,
            output_format=preview_output_format,
        )
        result.metadata = result.metadata or {}
        result.metadata["formats_with_previews"] = formats_with_previews

    return result

List supported creative formats.

Args
-----=
request
Request parameters
fetch_previews
If True, generate preview URLs for each format using sample manifests (uses batch API for 5-10x performance improvement)
preview_output_format
"url" for iframe URLs (default), "html" for direct embedding (2-3x faster, no iframe overhead)

Returns -----= TaskResult containing ListCreativeFormatsResponse with optional preview URLs in metadata

async def list_creatives(self,
request: ListCreativesRequest) ‑> TaskResult[ListCreativesResponse]
Expand source code
async def list_creatives(
    self,
    request: ListCreativesRequest,
) -> TaskResult[ListCreativesResponse]:
    """
    List Creatives.

    Args:
        request: Request parameters

    Returns:
        TaskResult containing ListCreativesResponse
    """
    dialect = self._creative_dialect(request, legacy_projection_available=True)
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="list_creatives",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.list_creatives(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="list_creatives",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    if dialect is CreativeDialect.CANONICAL:
        return self.adapter._parse_response(raw_result, ListCreativesResponse)
    return cast(
        TaskResult[ListCreativesResponse],
        self._canonicalize_format_read_result(
            raw_result,
            legacy_type=LegacyListCreativesResponse,
            canonical_type=ListCreativesResponse,
            collection="creatives",
            require_format=True,
        ),
    )

List Creatives.

Args
-----=
request
Request parameters

Returns -----= TaskResult containing ListCreativesResponse

async def list_creatives_legacy(self,
request: ListCreativesRequest) ‑> TaskResult[ListCreativesResponse]
Expand source code
async def list_creatives_legacy(
    self, request: LegacyListCreativesRequest
) -> TaskResult[LegacyListCreativesResponse]:
    """Return raw creative rows carrying legacy format identity."""

    self._warn_legacy_creative_api("list_creatives_legacy")
    raw = await self.adapter.list_creatives(request.model_dump(mode="json", exclude_none=True))
    return self.adapter._parse_response(raw, LegacyListCreativesResponse)

Return raw creative rows carrying legacy format identity.

async def list_products(self,
request: ListProductsRequest) ‑> TaskResult[Union[ListProductsResponse1, ListProductsResponse2]]
Expand source code
async def list_products(self, request: ListProductsRequest) -> TaskResult[ListProductsResponse]:
    """List products using the AdCP 3.2 compact discovery lifecycle."""
    return cast(
        TaskResult[ListProductsResponse],
        await self._execute_typed_task("list_products", request, ListProductsResponse),
    )

List products using the AdCP 3.2 compact discovery lifecycle.

async def list_property_lists(self, request: ListPropertyListsRequest) ‑> TaskResult[ListPropertyListsResponse]
Expand source code
async def list_property_lists(
    self,
    request: ListPropertyListsRequest,
) -> TaskResult[ListPropertyListsResponse]:
    """
    List property lists owned by a principal.

    Retrieves metadata for all property lists, optionally filtered
    by principal or pagination parameters.

    Args:
        request: Request parameters with optional filtering

    Returns:
        TaskResult containing ListPropertyListsResponse
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="list_property_lists",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.list_property_lists(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="list_property_lists",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, ListPropertyListsResponse)

List property lists owned by a principal.

Retrieves metadata for all property lists, optionally filtered by principal or pagination parameters.

Args
-----=
request
Request parameters with optional filtering

Returns -----= TaskResult containing ListPropertyListsResponse

async def list_tasks(self,
request: ListTasksRequest) ‑> TaskResult[ListTasksResponse]
Expand source code
async def list_tasks(
    self,
    request: ListTasksRequest,
) -> TaskResult[ListTasksResponse]:
    """
    List Tasks.

    Args:
        request: Request parameters

    Returns:
        TaskResult containing ListTasksResponse
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="list_tasks",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.list_tasks(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="list_tasks",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, ListTasksResponse)

List Tasks.

Args
-----=
request
Request parameters

Returns -----= TaskResult containing ListTasksResponse

async def list_tools(self) ‑> list[str]
Expand source code
async def list_tools(self) -> list[str]:
    """
    List available tools from the agent.

    Returns:
        List of tool names
    """
    return await self.adapter.list_tools()

List available tools from the agent.

Returns -----= List of tool names

async def list_transformers(self,
request: ListTransformersRequestCreativeAgent) ‑> TaskResult[ListTransformersResponseCreativeAgent]
Expand source code
async def list_transformers(
    self,
    request: ListTransformersRequest,
) -> TaskResult[ListTransformersResponse]:
    """
    List Creative Transformers.

    Args:
        request: Request parameters

    Returns:
        TaskResult containing ListTransformersResponse
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="list_transformers",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.list_transformers(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="list_transformers",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, ListTransformersResponse)

List Creative Transformers.

Args
-----=
request
Request parameters

Returns -----= TaskResult containing ListTransformersResponse

async def log_event(self,
request: LogEventRequest) ‑> TaskResult[Union[LogEventResponse1LogEventResponse2]]
Expand source code
async def log_event(
    self,
    request: LogEventRequest,
) -> TaskResult[LogEventResponse]:
    """
    Log Event.

    Args:
        request: Request parameters

    Returns:
        TaskResult containing LogEventResponse
    """
    self._validate_task_features("log_event")
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="log_event",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.log_event(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="log_event",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, LogEventResponse)

Log Event.

Args
-----=
request
Request parameters

Returns -----= TaskResult containing LogEventResponse

async def preview_creative_legacy(self,
request: PreviewCreativeRequest) ‑> TaskResult[Union[PreviewCreativeResponse1PreviewCreativeResponse2PreviewCreativeResponse3, PreviewCreativeResponse4]]
Expand source code
async def preview_creative_legacy(
    self,
    request: LegacyPreviewCreativeRequest,
) -> TaskResult[LegacyPreviewCreativeResponse]:
    """
    Generate preview of a creative manifest.

    Args:
        request: Request parameters

    Returns:
        TaskResult containing PreviewCreativeResponse with preview URLs
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="preview_creative",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.preview_creative(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="preview_creative",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    self._warn_legacy_creative_api("preview_creative_legacy")
    return self.adapter._parse_response(raw_result, LegacyPreviewCreativeResponse)

Generate preview of a creative manifest.

Args
-----=
request
Request parameters

Returns -----= TaskResult containing PreviewCreativeResponse with preview URLs

async def provide_performance_feedback(self,
request: ProvidePerformanceFeedbackRequest) ‑> TaskResult[Union[ProvidePerformanceFeedbackResponse1ProvidePerformanceFeedbackResponse2]]
Expand source code
async def provide_performance_feedback(
    self,
    request: ProvidePerformanceFeedbackRequest,
) -> TaskResult[ProvidePerformanceFeedbackResponse]:
    """
    Provide Performance Feedback.

    Args:
        request: Request parameters

    Returns:
        TaskResult containing ProvidePerformanceFeedbackResponse
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="provide_performance_feedback",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.provide_performance_feedback(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="provide_performance_feedback",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, ProvidePerformanceFeedbackResponse)

Provide Performance Feedback.

Args
-----=
request
Request parameters

Returns -----= TaskResult containing ProvidePerformanceFeedbackResponse

async def refine_proposals(self,
request: RefineProposalsRequest) ‑> TaskResult[Union[RefineProposalsResponse1, RefineProposalsResponse2]]
Expand source code
async def refine_proposals(
    self, request: RefineProposalsRequest
) -> TaskResult[RefineProposalsResponse]:
    """Refine one or more seller proposals."""
    return cast(
        TaskResult[RefineProposalsResponse],
        await self._execute_typed_task("refine_proposals", request, RefineProposalsResponse),
    )

Refine one or more seller proposals.

async def refresh_capabilities(self) ‑> adcp.types.generated_poc.protocol.get_adcp_capabilities_response.GetAdcpCapabilitiesResponse
Expand source code
async def refresh_capabilities(self) -> GetAdcpCapabilitiesResponse:
    """Fetch capabilities from the seller, bypassing cache.

    On strict-schema validation failure the raw response is inspected with
    ``looks_like_v3_capabilities``: if the agent is structurally v3-shaped,
    a wire-shape bug is surfaced loudly with the original validation error
    rather than silently downgrading to v2 (the v2 fallback would then ask
    for v2.5 schemas, which aren't shipped — one missing field would
    cascade into "AdCP schema data for version v2.5 not found"). Genuinely
    non-v3 responses still fall through to the transport-error path.

    Returns:
        The seller's capabilities response.

    Raises:
        ADCPError: On transport failure, or when the response is
            v3-shaped but fails schema validation. The error message
            explicitly references v3 in the latter case so the underlying
            wire-shape bug doesn't get blamed on a v2.5-schema cascade.
    """
    result = await self.get_adcp_capabilities(GetAdcpCapabilitiesRequest())
    if result.success and result.data is not None:
        self._capabilities = result.data
        self._feature_resolver = FeatureResolver(result.data)
        self._capabilities_fetched_at = time.monotonic()
        return self._capabilities

    # The typed call discards the raw payload on parse failure (only the
    # error string survives). Distinguish parse-failure (worth shape-
    # checking) from transport-failure (no data ever arrived) by the
    # error prefix produced by ProtocolAdapter._parse_response. Only on
    # parse-failure do we re-fetch the raw dict from the adapter to
    # inspect its shape; transport failures fall straight through to
    # the original error path.
    raw_data: Any = None
    is_parse_failure = result.error is not None and result.error.startswith(
        "Failed to parse response:"
    )
    if is_parse_failure:
        raw_result = await self.adapter.get_adcp_capabilities(
            GetAdcpCapabilitiesRequest().model_dump(mode="json", exclude_none=True)
        )
        raw_data = raw_result.data
        if isinstance(raw_data, list) and len(raw_data) == 1 and isinstance(raw_data[0], dict):
            # MCP content array — unwrap a single-item content envelope
            # so the heuristic sees the same shape the parser would.
            raw_data = raw_data[0]

    if looks_like_v3_capabilities(raw_data):
        logger.warning(
            "[AdCP] Agent %r returned a get_adcp_capabilities response that "
            "failed validation, but the response is structurally v3-shaped. "
            "The agent has a wire-shape bug — that's the thing to fix. "
            "(has_error=%s, has_data=%s)",
            self.agent_config.id,
            bool(result.error),
            raw_data is not None,
        )
        raise ADCPError(
            f"v3 capabilities response from agent {self.agent_config.id!r} "
            f"failed schema validation: {result.error or result.message}. "
            f"The response is structurally v3-shaped (carries `adcp`, "
            f"`supported_protocols`, or a v3 protocol block) — fix the "
            f"agent's wire shape rather than downgrading to v2.",
            agent_id=self.agent_config.id,
            agent_uri=self.agent_config.agent_uri,
        )

    raise ADCPError(
        f"Failed to fetch capabilities: {result.error or result.message}",
        agent_id=self.agent_config.id,
        agent_uri=self.agent_config.agent_uri,
    )

Fetch capabilities from the seller, bypassing cache.

On strict-schema validation failure the raw response is inspected with looks_like_v3_capabilities: if the agent is structurally v3-shaped, a wire-shape bug is surfaced loudly with the original validation error rather than silently downgrading to v2 (the v2 fallback would then ask for v2.5 schemas, which aren't shipped — one missing field would cascade into "AdCP schema data for version v2.5 not found"). Genuinely non-v3 responses still fall through to the transport-error path.

Returns -----= The seller's capabilities response.

Raises
-----=
ADCPError
On transport failure, or when the response is v3-shaped but fails schema validation. The error message explicitly references v3 in the latter case so the underlying wire-shape bug doesn't get blamed on a v2.5-schema cascade.
async def report_plan_adjustment(self,
request: ReportPlanAdjustmentRequest) ‑> TaskResult[ReportPlanAdjustmentResponse]
Expand source code
async def report_plan_adjustment(
    self,
    request: ReportPlanAdjustmentRequest,
) -> TaskResult[ReportPlanAdjustmentResponse]:
    """Report or review an adjustment to a governed plan outcome."""
    return cast(
        TaskResult[ReportPlanAdjustmentResponse],
        await self._execute_typed_task(
            "report_plan_adjustment", request, ReportPlanAdjustmentResponse
        ),
    )

Report or review an adjustment to a governed plan outcome.

async def report_plan_outcome(self,
request: ReportPlanOutcomeRequest) ‑> TaskResult[ReportPlanOutcomeResponse]
Expand source code
async def report_plan_outcome(
    self,
    request: ReportPlanOutcomeRequest,
) -> TaskResult[ReportPlanOutcomeResponse]:
    """Report the outcome of a governed action to the governance agent."""
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="report_plan_outcome",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.report_plan_outcome(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="report_plan_outcome",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, ReportPlanOutcomeResponse)

Report the outcome of a governed action to the governance agent.

async def report_usage(self,
request: ReportUsageRequest) ‑> TaskResult[ReportUsageResponse]
Expand source code
async def report_usage(
    self,
    request: ReportUsageRequest,
) -> TaskResult[ReportUsageResponse]:
    """
    Report Usage.

    Args:
        request: Request parameters

    Returns:
        TaskResult containing ReportUsageResponse
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="report_usage",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.report_usage(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="report_usage",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, ReportUsageResponse)

Report Usage.

Args
-----=
request
Request parameters

Returns -----= TaskResult containing ReportUsageResponse

async def request_proposals(self,
request: RequestProposalsRequest) ‑> TaskResult[Union[RequestProposalsResponse1, RequestProposalsResponse2, RequestProposalsResponse3, RequestProposalsResponse4]]
Expand source code
async def request_proposals(
    self, request: RequestProposalsRequest
) -> TaskResult[RequestProposalsResponse]:
    """Request seller proposals for selected products."""
    return cast(
        TaskResult[RequestProposalsResponse],
        await self._execute_typed_task("request_proposals", request, RequestProposalsResponse),
    )

Request seller proposals for selected products.

def require(self, *features: str) ‑> None
Expand source code
def require(self, *features: str) -> None:
    """Assert that the seller supports all listed features.

    Args:
        *features: Feature identifiers to require.

    Raises:
        ADCPFeatureUnsupportedError: If any features are not supported.
        ADCPError: If capabilities have not been fetched yet.
    """
    self._ensure_resolver().require(
        *features,
        agent_id=self.agent_config.id,
        agent_uri=self.agent_config.agent_uri,
    )

Assert that the seller supports all listed features.

Args
-----=
*features
Feature identifiers to require.
Raises
-----=
ADCPFeatureUnsupportedError
If any features are not supported.
ADCPError
If capabilities have not been fetched yet.
def reset_context(self, context_id: str | None = None) ‑> None
Expand source code
def reset_context(self, context_id: str | None = None) -> None:
    """Start a new A2A conversation on this client.

    Passing ``None`` (default) clears the current context so the
    server mints a fresh one on the next call. Passing a string uses
    it as the new conversation id — useful for resuming a specific
    prior session or for naming the conversation with your own
    correlation key. Note: some servers (notably ADK) rewrite
    client-supplied ids into their own session format; the client
    auto-adopts the rewritten id on the next response.

    Also clears any active_task_id — starting a new conversation
    discards any in-flight task on the old one.

    Raises ``TypeError`` when called on a non-A2A client.
    """
    if not isinstance(self.adapter, A2AAdapter):
        raise TypeError(
            f"reset_context is only supported for A2A protocol; "
            f"got {self.agent_config.protocol}"
        )
    self.adapter.set_context_id(context_id)

Start a new A2A conversation on this client.

Passing None (default) clears the current context so the server mints a fresh one on the next call. Passing a string uses it as the new conversation id — useful for resuming a specific prior session or for naming the conversation with your own correlation key. Note: some servers (notably ADK) rewrite client-supplied ids into their own session format; the client auto-adopts the rewritten id on the next response.

Also clears any active_task_id — starting a new conversation discards any in-flight task on the old one.

Raises TypeError when called on a non-A2A client.

async def si_get_offering(self, request: SiGetOfferingRequest) ‑> TaskResult[SiGetOfferingResponse]
Expand source code
async def si_get_offering(
    self,
    request: SiGetOfferingRequest,
) -> TaskResult[SiGetOfferingResponse]:
    """
    Get sponsored intelligence offering.

    Retrieves product/service offerings that can be presented in a
    sponsored intelligence session.

    Args:
        request: Request parameters including brand context

    Returns:
        TaskResult containing SiGetOfferingResponse
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="si_get_offering",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.si_get_offering(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="si_get_offering",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, SiGetOfferingResponse)

Get sponsored intelligence offering.

Retrieves product/service offerings that can be presented in a sponsored intelligence session.

Args
-----=
request
Request parameters including brand context

Returns -----= TaskResult containing SiGetOfferingResponse

async def si_initiate_session(self, request: SiInitiateSessionRequest) ‑> TaskResult[SiInitiateSessionResponse]
Expand source code
async def si_initiate_session(
    self,
    request: SiInitiateSessionRequest,
) -> TaskResult[SiInitiateSessionResponse]:
    """
    Initiate a sponsored intelligence session.

    Starts a conversational brand experience session with a user.

    Args:
        request: Request parameters including identity and context

    Returns:
        TaskResult containing SiInitiateSessionResponse with session_id
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="si_initiate_session",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.si_initiate_session(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="si_initiate_session",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, SiInitiateSessionResponse)

Initiate a sponsored intelligence session.

Starts a conversational brand experience session with a user.

Args
-----=
request
Request parameters including identity and context

Returns -----= TaskResult containing SiInitiateSessionResponse with session_id

async def si_send_message(self,
request: SiSendMessageRequest) ‑> TaskResult[SiSendMessageResponse]
Expand source code
async def si_send_message(
    self,
    request: SiSendMessageRequest,
) -> TaskResult[SiSendMessageResponse]:
    """
    Send a message in a sponsored intelligence session.

    Continues the conversation in an active SI session.

    Args:
        request: Request parameters including session_id and message

    Returns:
        TaskResult containing SiSendMessageResponse with brand response
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="si_send_message",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.si_send_message(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="si_send_message",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, SiSendMessageResponse)

Send a message in a sponsored intelligence session.

Continues the conversation in an active SI session.

Args
-----=
request
Request parameters including session_id and message

Returns -----= TaskResult containing SiSendMessageResponse with brand response

async def si_terminate_session(self, request: SiTerminateSessionRequest) ‑> TaskResult[SiTerminateSessionResponse]
Expand source code
async def si_terminate_session(
    self,
    request: SiTerminateSessionRequest,
) -> TaskResult[SiTerminateSessionResponse]:
    """
    Terminate a sponsored intelligence session.

    Ends an active SI session, optionally with follow-up actions.

    Args:
        request: Request parameters including session_id and termination context

    Returns:
        TaskResult containing SiTerminateSessionResponse
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="si_terminate_session",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.si_terminate_session(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="si_terminate_session",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, SiTerminateSessionResponse)

Terminate a sponsored intelligence session.

Ends an active SI session, optionally with follow-up actions.

Args
-----=
request
Request parameters including session_id and termination context

Returns -----= TaskResult containing SiTerminateSessionResponse

def supports(self, feature: str) ‑> bool
Expand source code
def supports(self, feature: str) -> bool:
    """Check if the seller supports a feature.

    Supports multiple feature namespaces:
    - Protocol support: ``supports("media_buy")`` checks ``supported_protocols``
    - Extension support: ``supports("ext:scope3")`` checks ``extensions_supported``
    - Targeting: ``supports("targeting.geo_countries")`` checks
      ``media_buy.execution.targeting``
    - Media buy features: ``supports("audience_targeting")`` checks
      ``media_buy.features``
    - Signals features: ``supports("catalog_signals")`` checks
      ``signals.features``

    Args:
        feature: Feature identifier to check.

    Returns:
        True if the seller declares the feature as supported.

    Raises:
        ADCPError: If capabilities have not been fetched yet.
    """
    return self._ensure_resolver().supports(feature)

Check if the seller supports a feature.

Supports multiple feature namespaces: - Protocol support: supports("media_buy") checks supported_protocols - Extension support: supports("ext:scope3") checks extensions_supported - Targeting: supports("targeting.geo_countries") checks media_buy.execution.targeting - Media buy features: supports("audience_targeting") checks media_buy.features - Signals features: supports("catalog_signals") checks signals.features

Args
-----=
feature
Feature identifier to check.

Returns -----= True if the seller declares the feature as supported.

Raises
-----=
ADCPError
If capabilities have not been fetched yet.
async def sync_accounts(self,
request: SyncAccountsRequest) ‑> TaskResult[Union[SyncAccountsResponse1SyncAccountsResponse2]]
Expand source code
async def sync_accounts(
    self,
    request: SyncAccountsRequest,
) -> TaskResult[SyncAccountsResponse]:
    """
    Sync Accounts.

    Args:
        request: Request parameters

    Returns:
        TaskResult containing SyncAccountsResponse
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="sync_accounts",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.sync_accounts(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="sync_accounts",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, SyncAccountsResponse)

Sync Accounts.

Args
-----=
request
Request parameters

Returns -----= TaskResult containing SyncAccountsResponse

async def sync_agent_notification_configs(self,
request: SyncAgentNotificationConfigsRequest) ‑> TaskResult[SyncAgentNotificationConfigsResponse]
Expand source code
async def sync_agent_notification_configs(
    self,
    request: SyncAgentNotificationConfigsRequest,
) -> TaskResult[SyncAgentNotificationConfigsResponse]:
    """Replace the caller-scoped agent notification subscriber set."""
    return cast(
        TaskResult[SyncAgentNotificationConfigsResponse],
        await self._execute_typed_task(
            "sync_agent_notification_configs",
            request,
            SyncAgentNotificationConfigsResponse,
        ),
    )

Replace the caller-scoped agent notification subscriber set.

async def sync_audiences(self,
request: SyncAudiencesRequest) ‑> TaskResult[Union[SyncAudiencesResponse1SyncAudiencesResponse2SyncAudiencesResponse3]]
Expand source code
async def sync_audiences(
    self,
    request: SyncAudiencesRequest,
) -> TaskResult[SyncAudiencesResponse]:
    """
    Sync Audiences.

    Args:
        request: Request parameters

    Returns:
        TaskResult containing SyncAudiencesResponse
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="sync_audiences",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.sync_audiences(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="sync_audiences",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, SyncAudiencesResponse)

Sync Audiences.

Args
-----=
request
Request parameters

Returns -----= TaskResult containing SyncAudiencesResponse

async def sync_catalogs(self,
request: SyncCatalogsRequest) ‑> TaskResult[Union[SyncCatalogsResponse1SyncCatalogsResponse2SyncCatalogsResponse3]]
Expand source code
async def sync_catalogs(
    self,
    request: SyncCatalogsRequest,
) -> TaskResult[SyncCatalogsResponse]:
    """
    Sync Catalogs.

    Args:
        request: Request parameters

    Returns:
        TaskResult containing SyncCatalogsResponse
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="sync_catalogs",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.sync_catalogs(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="sync_catalogs",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, SyncCatalogsResponse)

Sync Catalogs.

Args
-----=
request
Request parameters

Returns -----= TaskResult containing SyncCatalogsResponse

async def sync_creatives(self,
request: SyncCreativesRequest) ‑> TaskResult[Union[SyncCreativesResponse1SyncCreativesResponse2SyncCreativesResponse3]]
Expand source code
async def sync_creatives(
    self,
    request: SyncCreativesRequest,
) -> TaskResult[SyncCreativesResponse]:
    """
    Sync Creatives.

    Args:
        request: Request parameters

    Returns:
        TaskResult containing SyncCreativesResponse
    """
    dialect = self._creative_dialect(request, legacy_projection_available=True)
    operation_id = create_operation_id()
    params = self._prepare_creative_params(request)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="sync_creatives",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.sync_creatives(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="sync_creatives",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    if dialect is CreativeDialect.LEGACY:
        return cast(
            TaskResult[SyncCreativesResponse],
            self._canonicalize_lifecycle_result(
                raw_result,
                legacy_type=LegacySyncCreativesResponse,
                canonical_type=SyncCreativesResponse,
            ),
        )
    return self.adapter._parse_response(raw_result, SyncCreativesResponse)

Sync Creatives.

Args
-----=
request
Request parameters

Returns -----= TaskResult containing SyncCreativesResponse

async def sync_creatives_legacy(self,
request: SyncCreativesRequest) ‑> TaskResult[Union[SyncCreativesResponse1SyncCreativesResponse2SyncCreativesResponse3]]
Expand source code
async def sync_creatives_legacy(
    self, request: LegacySyncCreativesRequest
) -> TaskResult[LegacySyncCreativesResponse]:
    """Execute sync_creatives without the canonical application boundary."""

    self._warn_legacy_creative_api("sync_creatives_legacy")
    raw = await self.adapter.sync_creatives(request.model_dump(mode="json", exclude_none=True))
    return self.adapter._parse_response(raw, LegacySyncCreativesResponse)

Execute sync_creatives without the canonical application boundary.

async def sync_event_sources(self,
request: SyncEventSourcesRequest) ‑> TaskResult[Union[SyncEventSourcesResponse1SyncEventSourcesResponse2]]
Expand source code
async def sync_event_sources(
    self,
    request: SyncEventSourcesRequest,
) -> TaskResult[SyncEventSourcesResponse]:
    """
    Sync Event Sources.

    Args:
        request: Request parameters

    Returns:
        TaskResult containing SyncEventSourcesResponse
    """
    self._validate_task_features("sync_event_sources")
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="sync_event_sources",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.sync_event_sources(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="sync_event_sources",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, SyncEventSourcesResponse)

Sync Event Sources.

Args
-----=
request
Request parameters

Returns -----= TaskResult containing SyncEventSourcesResponse

async def sync_governance(self, request: SyncGovernanceRequest) ‑> TaskResult[SyncGovernanceResponse]
Expand source code
async def sync_governance(
    self,
    request: SyncGovernanceRequest,
) -> TaskResult[SyncGovernanceResponse]:
    """Sync governance agents attached to an account.

    Attach, detach, or replace the set of governance agents that must be
    consulted for plan approval on an account.

    Args:
        request: Request parameters with account and governance agents

    Returns:
        TaskResult containing SyncGovernanceResponse
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="sync_governance",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.sync_governance(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="sync_governance",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, SyncGovernanceResponse)

Sync governance agents attached to an account.

Attach, detach, or replace the set of governance agents that must be consulted for plan approval on an account.

Args
-----=
request
Request parameters with account and governance agents

Returns -----= TaskResult containing SyncGovernanceResponse

async def sync_plans(self,
request: SyncPlansRequest) ‑> TaskResult[SyncPlansResponse]
Expand source code
async def sync_plans(
    self,
    request: SyncPlansRequest,
) -> TaskResult[SyncPlansResponse]:
    """Sync campaign governance plans to the governance agent."""
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="sync_plans",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.sync_plans(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="sync_plans",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, SyncPlansResponse)

Sync campaign governance plans to the governance agent.

async def update_collection_list(self, request: UpdateCollectionListRequest) ‑> TaskResult[UpdateCollectionListResponse]
Expand source code
async def update_collection_list(
    self,
    request: UpdateCollectionListRequest,
) -> TaskResult[UpdateCollectionListResponse]:
    """Update a collection list.

    Args:
        request: Request parameters with list_id and updates

    Returns:
        TaskResult containing UpdateCollectionListResponse
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="update_collection_list",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.update_collection_list(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="update_collection_list",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, UpdateCollectionListResponse)

Update a collection list.

Args
-----=
request
Request parameters with list_id and updates

Returns -----= TaskResult containing UpdateCollectionListResponse

async def update_content_standards(self, request: UpdateContentStandardsRequest) ‑> TaskResult[UpdateContentStandardsResponse]
Expand source code
async def update_content_standards(
    self,
    request: UpdateContentStandardsRequest,
) -> TaskResult[UpdateContentStandardsResponse]:
    """
    Update a content standards configuration.

    Args:
        request: Request parameters including standards_id and updates

    Returns:
        TaskResult containing UpdateContentStandardsResponse
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="update_content_standards",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.update_content_standards(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="update_content_standards",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, UpdateContentStandardsResponse)

Update a content standards configuration.

Args
-----=
request
Request parameters including standards_id and updates

Returns -----= TaskResult containing UpdateContentStandardsResponse

async def update_media_buy(self,
request: UpdateMediaBuyRequest) ‑> TaskResult[Union[UpdateMediaBuyResponse1UpdateMediaBuyResponse2UpdateMediaBuyResponse3]]
Expand source code
async def update_media_buy(
    self,
    request: UpdateMediaBuyRequest,
) -> TaskResult[UpdateMediaBuyResponse]:
    """
    Update an existing media buy reservation.

    Modifies a previously created media buy by updating packages or publisher
    properties. The update operation uses discriminated unions to specify what
    to change - either package details or targeting properties.

    Args:
        request: Media buy update parameters including:
            - media_buy_id: Identifier from create_media_buy response
            - updates: Discriminated union specifying update type:
                * UpdateMediaBuyPackagesRequest: Modify package selections
                * UpdateMediaBuyPropertiesRequest: Change targeting properties

    Returns:
        TaskResult containing UpdateMediaBuyResponse with:
            - media_buy_id: The updated media buy identifier
            - status: Updated state of the media buy
            - packages: Updated package configurations
            - Additional platform-specific metadata

    Example:
        >>> from adcp import ADCPClient, UpdateMediaBuyPackagesRequest
        >>> client = ADCPClient(agent_config)
        >>> request = UpdateMediaBuyPackagesRequest(
        ...     media_buy_id="mb_123",
        ...     packages=[updated_package]
        ... )
        >>> result = await client.update_media_buy(request)
        >>> if result.success:
        ...     updated_packages = result.data.packages
    """
    dialect = self._creative_dialect(request, legacy_projection_available=True)
    operation_id = create_operation_id()
    params = self._prepare_creative_params(request)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="update_media_buy",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.update_media_buy(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="update_media_buy",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    if dialect is CreativeDialect.LEGACY:
        return cast(
            TaskResult[UpdateMediaBuyResponse],
            self._canonicalize_lifecycle_result(
                raw_result,
                legacy_type=LegacyUpdateMediaBuyResponse,
                canonical_type=UpdateMediaBuyResponse,
            ),
        )
    return self.adapter._parse_response(raw_result, UpdateMediaBuyResponse)

Update an existing media buy reservation.

Modifies a previously created media buy by updating packages or publisher properties. The update operation uses discriminated unions to specify what to change - either package details or targeting properties.

Args
-----=
request
Media buy update parameters including: - media_buy_id: Identifier from create_media_buy response - updates: Discriminated union specifying update type: * UpdateMediaBuyPackagesRequest: Modify package selections * UpdateMediaBuyPropertiesRequest: Change targeting properties

Returns -----= TaskResult containing UpdateMediaBuyResponse with: - media_buy_id: The updated media buy identifier - status: Updated state of the media buy - packages: Updated package configurations - Additional platform-specific metadata

Example -----=

>>> from adcp import ADCPClient, UpdateMediaBuyPackagesRequest
>>> client = ADCPClient(agent_config)
>>> request = UpdateMediaBuyPackagesRequest(
...     media_buy_id="mb_123",
...     packages=[updated_package]
... )
>>> result = await client.update_media_buy(request)
>>> if result.success:
...     updated_packages = result.data.packages
async def update_media_buy_legacy(self,
request: UpdateMediaBuyRequest) ‑> TaskResult[Union[UpdateMediaBuyResponse1UpdateMediaBuyResponse2UpdateMediaBuyResponse3]]
Expand source code
async def update_media_buy_legacy(
    self, request: LegacyUpdateMediaBuyRequest
) -> TaskResult[LegacyUpdateMediaBuyResponse]:
    """Execute update_media_buy without the canonical application boundary."""

    self._warn_legacy_creative_api("update_media_buy_legacy")
    raw = await self.adapter.update_media_buy(
        request.model_dump(mode="json", exclude_none=True)
    )
    return self.adapter._parse_response(raw, LegacyUpdateMediaBuyResponse)

Execute update_media_buy without the canonical application boundary.

async def update_property_list(self, request: UpdatePropertyListRequest) ‑> TaskResult[UpdatePropertyListResponse]
Expand source code
async def update_property_list(
    self,
    request: UpdatePropertyListRequest,
) -> TaskResult[UpdatePropertyListResponse]:
    """
    Update a property list.

    Modifies the filters, brand manifest, or other parameters
    of an existing property list.

    Args:
        request: Request parameters with list_id and updates

    Returns:
        TaskResult containing UpdatePropertyListResponse
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="update_property_list",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.update_property_list(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="update_property_list",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, UpdatePropertyListResponse)

Update a property list.

Modifies the filters, brand manifest, or other parameters of an existing property list.

Args
-----=
request
Request parameters with list_id and updates

Returns -----= TaskResult containing UpdatePropertyListResponse

async def update_rights(self, request: UpdateRightsRequest) ‑> TaskResult[Union[UpdateRightsResponse1, UpdateRightsResponse2]]
Expand source code
async def update_rights(
    self,
    request: UpdateRightsRequest,
) -> TaskResult[UpdateRightsResponse]:
    """Update terms of an existing rights acquisition.

    Modifies a previously acquired rights record — typically to extend
    the ``end_date``, raise the ``impression_cap``, pause/unpause via
    ``paused``, or swap to a compatible ``pricing_option_id``. Partial
    update: pass only the fields you want to change.

    Failure modes (surface as ``TaskResult`` with ``success=False``):

    * Acquisition is expired or revoked — the seller rejects the update
      outright; mint a fresh ``acquire_rights`` instead.
    * ``pricing_option_id`` swap to an incompatible option — rejected;
      the new option's terms must be a strict superset / compatible
      with the original acquisition.
    * No partial-state mutations on rejection: the acquisition remains
      at its prior state when any field fails validation.

    Args:
        request: Request with ``rights_id`` and at least one mutable
            field (``end_date``, ``impression_cap``, ``paused``, or
            ``pricing_option_id``).

    Returns:
        TaskResult containing UpdateRightsResponse (updated or error).
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="update_rights",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.update_rights(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="update_rights",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, UpdateRightsResponse)

Update terms of an existing rights acquisition.

Modifies a previously acquired rights record — typically to extend the end_date, raise the impression_cap, pause/unpause via paused, or swap to a compatible pricing_option_id. Partial update: pass only the fields you want to change.

Failure modes (surface as TaskResult with success=False):

  • Acquisition is expired or revoked — the seller rejects the update outright; mint a fresh acquire_rights instead.
  • pricing_option_id swap to an incompatible option — rejected; the new option's terms must be a strict superset / compatible with the original acquisition.
  • No partial-state mutations on rejection: the acquisition remains at its prior state when any field fails validation.
Args
-----=
request
Request with rights_id and at least one mutable field (end_date, impression_cap, paused, or pricing_option_id).

Returns -----= TaskResult containing UpdateRightsResponse (updated or error).

def use_idempotency_key(self, key: str) ‑> Iterator[str]
Expand source code
@contextlib.contextmanager
def use_idempotency_key(self, key: str) -> Iterator[str]:
    """Pin an ``idempotency_key`` for the next mutating call on THIS client.

    Use when you've persisted a key (e.g., in a buyer-side database) and
    want the SDK to send that exact key on resume or retry across process
    restarts. The key is validated against ``^[A-Za-z0-9_.:-]{16,255}$`` on
    entry; a ``ValueError`` is raised for malformed keys.

    Scope rules:

    * **Single-use within scope.** The first mutating call inside the
      ``with`` block consumes the pinned key; a second mutating call falls
      through to a fresh UUID. This protects against ``asyncio.gather``
      siblings accidentally sharing the key (which would trigger
      ``IDEMPOTENCY_CONFLICT`` or silently duplicate work). If you need to
      retry, wrap each attempt in its own ``with`` block.
    * **Client-scoped.** The pinned key applies only to calls on THIS
      client. A mutating call on a sibling ``ADCPClient`` inside the same
      ``with`` block generates a fresh key and emits a ``UserWarning`` —
      keys must be unique per (seller, request) pair (AdCP #2315).
    * **No nesting.** Nested ``use_idempotency_key`` on the same client
      raises ``RuntimeError``.

    Example::

        with client.use_idempotency_key(campaign.stored_key):
            result = await client.create_media_buy(request)
    """
    from adcp import _idempotency

    _idempotency.validate_key(key)
    token = self._idempotency_client_token
    if token in _idempotency._scoped_keys:
        raise RuntimeError(
            "use_idempotency_key is already active on this client; "
            "nested usage is not supported."
        )
    _idempotency._scoped_keys[token] = key
    try:
        yield key
    finally:
        _idempotency._scoped_keys.pop(token, None)

Pin an idempotency_key for the next mutating call on THIS client.

Use when you've persisted a key (e.g., in a buyer-side database) and want the SDK to send that exact key on resume or retry across process restarts. The key is validated against ^[A-Za-z0-9_.:-]{16,255}$ on entry; a ValueError is raised for malformed keys.

Scope rules:

  • Single-use within scope. The first mutating call inside the with block consumes the pinned key; a second mutating call falls through to a fresh UUID. This protects against asyncio.gather siblings accidentally sharing the key (which would trigger IDEMPOTENCY_CONFLICT or silently duplicate work). If you need to retry, wrap each attempt in its own with block.
  • Client-scoped. The pinned key applies only to calls on THIS client. A mutating call on a sibling ADCPClient inside the same with block generates a fresh key and emits a UserWarning — keys must be unique per (seller, request) pair (AdCP #2315).
  • No nesting. Nested use_idempotency_key on the same client raises RuntimeError.

Example::

with client.use_idempotency_key(campaign.stored_key):
    result = await client.create_media_buy(request)
async def validate_content_delivery(self, request: ValidateContentDeliveryRequest) ‑> TaskResult[Union[ValidateContentDeliveryResponse1ValidateContentDeliveryResponse2]]
Expand source code
async def validate_content_delivery(
    self,
    request: ValidateContentDeliveryRequest,
) -> TaskResult[ValidateContentDeliveryResponse]:
    """
    Validate content delivery against standards.

    Validates that ad delivery records comply with content standards.

    Args:
        request: Request parameters including delivery records

    Returns:
        TaskResult containing ValidateContentDeliveryResponse
    """
    operation_id = create_operation_id()
    params = request.model_dump(mode="json", exclude_none=True)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_REQUEST,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="validate_content_delivery",
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    raw_result = await self.adapter.validate_content_delivery(params)

    self._emit_activity(
        Activity(
            type=ActivityType.PROTOCOL_RESPONSE,
            operation_id=operation_id,
            agent_id=self.agent_config.id,
            task_type="validate_content_delivery",
            status=raw_result.status,
            timestamp=datetime.now(timezone.utc).isoformat(),
        )
    )

    return self.adapter._parse_response(raw_result, ValidateContentDeliveryResponse)

Validate content delivery against standards.

Validates that ad delivery records comply with content standards.

Args
-----=
request
Request parameters including delivery records

Returns -----= TaskResult containing ValidateContentDeliveryResponse

async def validate_input(self, request: Any) ‑> TaskResult[Any]
Expand source code
async def validate_input(self, request: Any) -> TaskResult[Any]:
    """Validate creative input against a format declaration."""
    from adcp.types import _generated as gen

    params = request.model_dump(mode="json", exclude_none=True)
    raw_result = await self.adapter.validate_input(params)
    return self.adapter._parse_response(raw_result, gen.ValidateInputResponse)

Validate creative input against a format declaration.

async def verify_brand_claim(self, request: Any) ‑> TaskResult[Any]
Expand source code
async def verify_brand_claim(self, request: Any) -> TaskResult[Any]:
    """Verify a single brand claim."""
    from adcp.types import _generated as gen

    params = request.model_dump(mode="json", exclude_none=True)
    raw_result = await self.adapter.verify_brand_claim(params)
    return self.adapter._parse_response(raw_result, gen.VerifyBrandClaimResponse)

Verify a single brand claim.

async def verify_brand_claims(self, request: Any) ‑> TaskResult[Any]
Expand source code
async def verify_brand_claims(self, request: Any) -> TaskResult[Any]:
    """Verify multiple brand claims."""
    from adcp.types import _generated as gen

    params = request.model_dump(mode="json", exclude_none=True)
    raw_result = await self.adapter.verify_brand_claims(params)
    return self.adapter._parse_response(raw_result, gen.VerifyBrandClaimsResponseBulk)

Verify multiple brand claims.

class ADCPConnectionError (message: str, agent_id: str | None = None, agent_uri: str | None = None)
Expand source code
class ADCPConnectionError(ADCPError):
    """Connection to agent failed."""

    def __init__(self, message: str, agent_id: str | None = None, agent_uri: str | None = None):
        """Initialize connection error."""
        suggestion = (
            "Check that the agent URI is correct and the agent is running.\n"
            "     Try testing with: python -m adcp test --config <agent-id>"
        )
        super().__init__(message, agent_id, agent_uri, suggestion)

    @property
    def is_retryable(self) -> bool:
        return True

Connection to agent failed.

Initialize connection error.

Ancestors

  • ADCPError
  • builtins.Exception
  • builtins.BaseException

Inherited members

class ADCPError (message: str,
agent_id: str | None = None,
agent_uri: str | None = None,
suggestion: str | None = None)
Expand source code
class ADCPError(Exception):
    """Base exception for all AdCP client errors."""

    def __init__(
        self,
        message: str,
        agent_id: str | None = None,
        agent_uri: str | None = None,
        suggestion: str | None = None,
    ):
        """Initialize exception with context."""
        self.message = message
        self.agent_id = agent_id
        self.agent_uri = agent_uri
        self.suggestion = suggestion

        full_message = message
        if agent_id:
            full_message = f"[Agent: {agent_id}] {full_message}"
        if agent_uri:
            full_message = f"{full_message}\n  URI: {agent_uri}"
        if suggestion:
            full_message = f"{full_message}\n  Suggestion: {suggestion}"

        super().__init__(full_message)

    @property
    def is_retryable(self) -> bool:
        """Whether this error is safe to retry."""
        return False

Base exception for all AdCP client errors.

Initialize exception with context.

Ancestors

  • builtins.Exception
  • builtins.BaseException

Subclasses

Instance variables

prop is_retryable : bool
Expand source code
@property
def is_retryable(self) -> bool:
    """Whether this error is safe to retry."""
    return False

Whether this error is safe to retry.

class ADCPFeatureUnsupportedError (unsupported_features: list[str],
declared_features: list[str] | None = None,
agent_id: str | None = None,
agent_uri: str | None = None)
Expand source code
class ADCPFeatureUnsupportedError(ADCPError):
    """Seller does not support one or more required features."""

    def __init__(
        self,
        unsupported_features: list[str],
        declared_features: list[str] | None = None,
        agent_id: str | None = None,
        agent_uri: str | None = None,
    ):
        """Initialize feature unsupported error.

        Args:
            unsupported_features: Features that are not supported.
            declared_features: Features the seller does declare.
            agent_id: Optional agent ID for context.
            agent_uri: Optional agent URI for context.
        """
        self.unsupported_features = unsupported_features
        self.declared_features = declared_features or []

        missing = ", ".join(unsupported_features)
        message = f"Seller does not support: {missing}"

        suggestion = None
        if self.declared_features:
            declared = ", ".join(sorted(self.declared_features))
            suggestion = f"Declared features: {declared}"

        super().__init__(message, agent_id, agent_uri, suggestion)

Seller does not support one or more required features.

Initialize feature unsupported error.

Args
-----=
unsupported_features
Features that are not supported.
declared_features
Features the seller does declare.
agent_id
Optional agent ID for context.
agent_uri
Optional agent URI for context.

Ancestors

  • ADCPError
  • builtins.Exception
  • builtins.BaseException

Inherited members

class ADCPMultiAgentClient (agents: list[AgentConfig],
webhook_url_template: str | None = None,
webhook_secret: str | None = None,
on_activity: Callable[[Activity], None] | None = None,
handlers: dict[str, Callable[..., Any]] | None = None,
signing: SigningConfig | None = None,
adcp_version: str | dict[str, str] | None = None,
legacy_format_converter: LegacyFormatConverter | None = None,
canonical_format_legacy_resolver: CanonicalFormatLegacyResolver | None = None,
allow_unauthenticated_webhooks: bool | Mapping[str, bool] = False)
Expand source code
class ADCPMultiAgentClient:
    """Client for managing multiple AdCP agents."""

    def __init__(
        self,
        agents: list[AgentConfig],
        webhook_url_template: str | None = None,
        webhook_secret: str | None = None,
        on_activity: Callable[[Activity], None] | None = None,
        handlers: dict[str, Callable[..., Any]] | None = None,
        signing: SigningConfig | None = None,
        adcp_version: str | dict[str, str] | None = None,
        legacy_format_converter: LegacyFormatConverter | None = None,
        canonical_format_legacy_resolver: CanonicalFormatLegacyResolver | None = None,
        allow_unauthenticated_webhooks: bool | Mapping[str, bool] = False,
    ):
        """
        Initialize multi-agent client.

        Args:
            agents: List of agent configurations
            webhook_url_template: Template for webhook URLs
            webhook_secret: Shared secret for the deprecated HMAC-SHA256 webhook
                fallback. Configure only for registrations that explicitly
                selected legacy HMAC; use ``WebhookReceiver`` for RFC 9421.
            on_activity: Callback for activity events
            handlers: Task completion handlers
            signing: Optional RFC 9421 signing config forwarded to every
                per-agent ADCPClient. The same identity signs traffic to
                all agents. See ADCPClient.__init__ for details.
            allow_unauthenticated_webhooks: Explicit compatibility escape.
                A mapping scopes the opt-in by agent ID; omitted IDs remain
                protected. A uniform True is accepted only for a single-agent
                collection. Defaults to False.
            adcp_version: AdCP protocol release pin. Three forms:

                - ``None`` (default): every per-agent ADCPClient resolves
                  the SDK's compile-time pin.
                - ``str`` (e.g. ``"3.1"``): every agent uses this pin.
                - ``dict[str, str]`` (e.g.
                  ``{"seller_a": "3.0", "seller_b": "3.1"}``): per-agent
                  override map keyed by ``agent.id``. Agents missing
                  from the map fall back to the SDK default — useful
                  for holdco/multi-tenant operators where one seller is
                  ahead of the others on the upgrade cadence.

                See ADCPClient.__init__ for per-instance semantics.
                Cross-major pins raise ConfigurationError at construction.
        """
        agent_ids = {agent.id for agent in agents}
        if isinstance(allow_unauthenticated_webhooks, Mapping):
            unknown_ids = set(allow_unauthenticated_webhooks) - agent_ids
            if unknown_ids:
                unknown = ", ".join(sorted(unknown_ids))
                raise ValueError(
                    "allow_unauthenticated_webhooks contains unknown agent IDs: " + unknown
                )
            if any(type(value) is not bool for value in allow_unauthenticated_webhooks.values()):
                raise TypeError("allow_unauthenticated_webhooks mapping values must be bools")
            per_agent_unauthenticated = dict(allow_unauthenticated_webhooks)
        else:
            if type(allow_unauthenticated_webhooks) is not bool:
                raise TypeError(
                    "allow_unauthenticated_webhooks must be a bool or mapping of agent IDs to bools"
                )
            if allow_unauthenticated_webhooks is True and len(agents) > 1:
                raise ValueError(
                    "allow_unauthenticated_webhooks=True cannot be applied to multiple agents; "
                    "pass a mapping keyed by the isolated agent IDs"
                )
            per_agent_unauthenticated = {
                agent.id: allow_unauthenticated_webhooks for agent in agents
            }

        # Per-agent map → resolve each pin individually for the dict form;
        # otherwise use the uniform pin for all agents.
        if isinstance(adcp_version, dict):
            self._adcp_version: str | None = None  # mixed pins
            self._per_agent_versions: dict[str, str] = {
                agent_id: resolve_adcp_version(pin) for agent_id, pin in adcp_version.items()
            }
            default_pin = resolve_adcp_version(None)
            self.agents = {
                agent.id: ADCPClient(
                    agent,
                    webhook_url_template=webhook_url_template,
                    webhook_secret=webhook_secret,
                    on_activity=on_activity,
                    signing=signing,
                    adcp_version=self._per_agent_versions.get(agent.id, default_pin),
                    legacy_format_converter=legacy_format_converter,
                    canonical_format_legacy_resolver=canonical_format_legacy_resolver,
                    allow_unauthenticated_webhooks=per_agent_unauthenticated.get(agent.id, False),
                )
                for agent in agents
            }
        else:
            self._adcp_version = resolve_adcp_version(adcp_version)
            self._per_agent_versions = {}
            self.agents = {
                agent.id: ADCPClient(
                    agent,
                    webhook_url_template=webhook_url_template,
                    webhook_secret=webhook_secret,
                    on_activity=on_activity,
                    signing=signing,
                    adcp_version=self._adcp_version,
                    legacy_format_converter=legacy_format_converter,
                    canonical_format_legacy_resolver=canonical_format_legacy_resolver,
                    allow_unauthenticated_webhooks=per_agent_unauthenticated.get(agent.id, False),
                )
                for agent in agents
            }
        self.handlers = handlers or {}

    def get_adcp_version(self) -> str:
        """Return the AdCP protocol release pin for this multi-client.

        Returns the uniform pin when all agents share one. Raises
        :class:`ValueError` when agents have heterogeneous pins (the
        ``dict[str, str]`` constructor form) — in that case, query
        the per-agent pin via ``multi.agent(agent_id).get_adcp_version()``.
        """
        if self._adcp_version is not None:
            return self._adcp_version
        # Heterogeneous: surface uniformly if all agents agree at runtime.
        versions = {client.get_adcp_version() for client in self.agents.values()}
        if len(versions) == 1:
            return next(iter(versions))
        raise ValueError(
            "Multi-agent client has heterogeneous adcp_version pins; "
            "use multi.agent(agent_id).get_adcp_version() to read per-agent. "
            f"Pins by agent: { {a: c.get_adcp_version() for a, c in self.agents.items()} }"
        )

    def agent(self, agent_id: str) -> ADCPClient:
        """Get client for specific agent."""
        if agent_id not in self.agents:
            raise ValueError(f"Agent not found: {agent_id}")
        return self.agents[agent_id]

    @property
    def agent_ids(self) -> list[str]:
        """Get list of agent IDs."""
        return list(self.agents.keys())

    async def close(self) -> None:
        """Close all agent clients and clean up resources."""
        import asyncio

        logger.debug("Closing all agent clients in multi-agent client")
        close_tasks = [client.close() for client in self.agents.values()]
        await asyncio.gather(*close_tasks, return_exceptions=True)

    async def __aenter__(self) -> ADCPMultiAgentClient:
        """Async context manager entry."""
        return self

    async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
        """Async context manager exit."""
        await self.close()

    async def get_products(
        self,
        request: GetProductsRequest,
    ) -> list[TaskResult[GetProductsResponse]]:
        """
        Execute get_products across all agents in parallel.

        Args:
            request: Request parameters

        Returns:
            List of TaskResults containing GetProductsResponse for each agent
        """
        import asyncio

        tasks = [agent.get_products(request) for agent in self.agents.values()]
        return await asyncio.gather(*tasks)

    async def list_products(
        self, request: ListProductsRequest
    ) -> list[TaskResult[ListProductsResponse]]:
        """Execute compact product discovery across all agents."""
        import asyncio

        return await asyncio.gather(
            *(agent.list_products(request) for agent in self.agents.values())
        )

    async def request_proposals(
        self, request: RequestProposalsRequest
    ) -> list[TaskResult[RequestProposalsResponse]]:
        """Request proposals from all agents."""
        import asyncio

        return await asyncio.gather(
            *(agent.request_proposals(request) for agent in self.agents.values())
        )

    async def refine_proposals(
        self, request: RefineProposalsRequest
    ) -> list[TaskResult[RefineProposalsResponse]]:
        """Refine proposals across all agents."""
        import asyncio

        return await asyncio.gather(
            *(agent.refine_proposals(request) for agent in self.agents.values())
        )

    async def decline_proposals(
        self, request: DeclineProposalsRequest
    ) -> list[TaskResult[DeclineProposalsResponse]]:
        """Decline proposals across all agents."""
        import asyncio

        return await asyncio.gather(
            *(agent.decline_proposals(request) for agent in self.agents.values())
        )

    async def buy_products(
        self, request: BuyProductsRequest
    ) -> list[TaskResult[BuyProductsResponse]]:
        """Commit direct purchases across all agents."""
        import asyncio

        return await asyncio.gather(
            *(agent.buy_products(request) for agent in self.agents.values())
        )

    async def accept_proposal(
        self, request: AcceptProposalRequest
    ) -> list[TaskResult[AcceptProposalResponse]]:
        """Accept proposals across all agents."""
        import asyncio

        return await asyncio.gather(
            *(agent.accept_proposal(request) for agent in self.agents.values())
        )

    async def control_media_buy(
        self, request: ControlMediaBuyRequest
    ) -> list[TaskResult[ControlMediaBuyResponse]]:
        """Control media buys across all agents."""
        import asyncio

        return await asyncio.gather(
            *(agent.control_media_buy(request) for agent in self.agents.values())
        )

    async def get_products_legacy(
        self, request: LegacyGetProductsRequest
    ) -> list[TaskResult[LegacyGetProductsResponse]]:
        """Execute the explicit raw discovery adapter across all agents."""

        import asyncio

        return await asyncio.gather(
            *(agent.get_products_legacy(request) for agent in self.agents.values())
        )

    async def execute_task(self, task_name: str, request: BaseModel) -> list[TaskResult[Any]]:
        """Execute a canonical task across all configured agents."""

        import asyncio

        return await asyncio.gather(
            *(agent.execute_task(task_name, request) for agent in self.agents.values())
        )

    async def execute_task_legacy(
        self, task_name: str, request: BaseModel
    ) -> list[TaskResult[Any]]:
        """Execute an explicit raw task adapter across all configured agents."""

        import asyncio

        return await asyncio.gather(
            *(agent.execute_task_legacy(task_name, request) for agent in self.agents.values())
        )

    @classmethod
    def from_env(cls) -> ADCPMultiAgentClient:
        """Create client from environment variables."""
        agents_json = os.getenv("ADCP_AGENTS")
        if not agents_json:
            raise ValueError("ADCP_AGENTS environment variable not set")

        agents_data = json.loads(agents_json)
        agents = [AgentConfig(**agent) for agent in agents_data]

        return cls(
            agents=agents,
            webhook_url_template=os.getenv("WEBHOOK_URL_TEMPLATE"),
            webhook_secret=os.getenv("WEBHOOK_SECRET"),
        )

Client for managing multiple AdCP agents.

Initialize multi-agent client.

Args
-----=
agents
List of agent configurations
webhook_url_template
Template for webhook URLs
webhook_secret
Shared secret for the deprecated HMAC-SHA256 webhook fallback. Configure only for registrations that explicitly selected legacy HMAC; use WebhookReceiver for RFC 9421.
on_activity
Callback for activity events
handlers
Task completion handlers
signing
Optional RFC 9421 signing config forwarded to every per-agent ADCPClient. The same identity signs traffic to all agents. See ADCPClient.init for details.
allow_unauthenticated_webhooks
Explicit compatibility escape. A mapping scopes the opt-in by agent ID; omitted IDs remain protected. A uniform True is accepted only for a single-agent collection. Defaults to False.
adcp_version

AdCP protocol release pin. Three forms:

  • None (default): every per-agent ADCPClient resolves the SDK's compile-time pin.
  • str (e.g. "3.1"): every agent uses this pin.
  • dict[str, str] (e.g. {"seller_a": "3.0", "seller_b": "3.1"}): per-agent override map keyed by agent.id. Agents missing from the map fall back to the SDK default — useful for holdco/multi-tenant operators where one seller is ahead of the others on the upgrade cadence.

See ADCPClient.init for per-instance semantics. Cross-major pins raise ConfigurationError at construction.

Static methods

def from_env() ‑> ADCPMultiAgentClient

Create client from environment variables.

Instance variables

prop agent_ids : list[str]
Expand source code
@property
def agent_ids(self) -> list[str]:
    """Get list of agent IDs."""
    return list(self.agents.keys())

Get list of agent IDs.

Methods

async def accept_proposal(self,
request: AcceptProposalRequest) ‑> list[TaskResult[Union[AcceptProposalResponse5, AcceptProposalResponse6, AcceptProposalResponse7]]]
Expand source code
async def accept_proposal(
    self, request: AcceptProposalRequest
) -> list[TaskResult[AcceptProposalResponse]]:
    """Accept proposals across all agents."""
    import asyncio

    return await asyncio.gather(
        *(agent.accept_proposal(request) for agent in self.agents.values())
    )

Accept proposals across all agents.

def agent(self, agent_id: str) ‑> ADCPClient
Expand source code
def agent(self, agent_id: str) -> ADCPClient:
    """Get client for specific agent."""
    if agent_id not in self.agents:
        raise ValueError(f"Agent not found: {agent_id}")
    return self.agents[agent_id]

Get client for specific agent.

async def buy_products(self,
request: BuyProductsRequest) ‑> list[TaskResult[Union[BuyProductsResponse5, BuyProductsResponse6, BuyProductsResponse7]]]
Expand source code
async def buy_products(
    self, request: BuyProductsRequest
) -> list[TaskResult[BuyProductsResponse]]:
    """Commit direct purchases across all agents."""
    import asyncio

    return await asyncio.gather(
        *(agent.buy_products(request) for agent in self.agents.values())
    )

Commit direct purchases across all agents.

async def close(self) ‑> None
Expand source code
async def close(self) -> None:
    """Close all agent clients and clean up resources."""
    import asyncio

    logger.debug("Closing all agent clients in multi-agent client")
    close_tasks = [client.close() for client in self.agents.values()]
    await asyncio.gather(*close_tasks, return_exceptions=True)

Close all agent clients and clean up resources.

async def control_media_buy(self,
request: ControlMediaBuyRequest) ‑> list[TaskResult[Union[ControlMediaBuyResponse1, ControlMediaBuyResponse2, ControlMediaBuyResponse3]]]
Expand source code
async def control_media_buy(
    self, request: ControlMediaBuyRequest
) -> list[TaskResult[ControlMediaBuyResponse]]:
    """Control media buys across all agents."""
    import asyncio

    return await asyncio.gather(
        *(agent.control_media_buy(request) for agent in self.agents.values())
    )

Control media buys across all agents.

async def decline_proposals(self,
request: DeclineProposalsRequest) ‑> list[TaskResult[Union[DeclineProposalsResponse1, DeclineProposalsResponse2]]]
Expand source code
async def decline_proposals(
    self, request: DeclineProposalsRequest
) -> list[TaskResult[DeclineProposalsResponse]]:
    """Decline proposals across all agents."""
    import asyncio

    return await asyncio.gather(
        *(agent.decline_proposals(request) for agent in self.agents.values())
    )

Decline proposals across all agents.

async def execute_task(self, task_name: str, request: BaseModel) ‑> list[TaskResult[Any]]
Expand source code
async def execute_task(self, task_name: str, request: BaseModel) -> list[TaskResult[Any]]:
    """Execute a canonical task across all configured agents."""

    import asyncio

    return await asyncio.gather(
        *(agent.execute_task(task_name, request) for agent in self.agents.values())
    )

Execute a canonical task across all configured agents.

async def execute_task_legacy(self, task_name: str, request: BaseModel) ‑> list[TaskResult[Any]]
Expand source code
async def execute_task_legacy(
    self, task_name: str, request: BaseModel
) -> list[TaskResult[Any]]:
    """Execute an explicit raw task adapter across all configured agents."""

    import asyncio

    return await asyncio.gather(
        *(agent.execute_task_legacy(task_name, request) for agent in self.agents.values())
    )

Execute an explicit raw task adapter across all configured agents.

def get_adcp_version(self) ‑> str
Expand source code
def get_adcp_version(self) -> str:
    """Return the AdCP protocol release pin for this multi-client.

    Returns the uniform pin when all agents share one. Raises
    :class:`ValueError` when agents have heterogeneous pins (the
    ``dict[str, str]`` constructor form) — in that case, query
    the per-agent pin via ``multi.agent(agent_id).get_adcp_version()``.
    """
    if self._adcp_version is not None:
        return self._adcp_version
    # Heterogeneous: surface uniformly if all agents agree at runtime.
    versions = {client.get_adcp_version() for client in self.agents.values()}
    if len(versions) == 1:
        return next(iter(versions))
    raise ValueError(
        "Multi-agent client has heterogeneous adcp_version pins; "
        "use multi.agent(agent_id).get_adcp_version() to read per-agent. "
        f"Pins by agent: { {a: c.get_adcp_version() for a, c in self.agents.items()} }"
    )

Return the AdCP protocol release pin for this multi-client.

Returns the uniform pin when all agents share one. Raises :class:ValueError when agents have heterogeneous pins (the dict[str, str] constructor form) — in that case, query the per-agent pin via multi.agent(agent_id).get_adcp_version().

async def get_products(self,
request: GetProductsRequest) ‑> list[TaskResult[GetProductsResponse]]
Expand source code
async def get_products(
    self,
    request: GetProductsRequest,
) -> list[TaskResult[GetProductsResponse]]:
    """
    Execute get_products across all agents in parallel.

    Args:
        request: Request parameters

    Returns:
        List of TaskResults containing GetProductsResponse for each agent
    """
    import asyncio

    tasks = [agent.get_products(request) for agent in self.agents.values()]
    return await asyncio.gather(*tasks)

Execute get_products across all agents in parallel.

Args
-----=
request
Request parameters

Returns -----= List of TaskResults containing GetProductsResponse for each agent

async def get_products_legacy(self,
request: GetProductsRequest) ‑> list[TaskResult[GetProductsResponse]]
Expand source code
async def get_products_legacy(
    self, request: LegacyGetProductsRequest
) -> list[TaskResult[LegacyGetProductsResponse]]:
    """Execute the explicit raw discovery adapter across all agents."""

    import asyncio

    return await asyncio.gather(
        *(agent.get_products_legacy(request) for agent in self.agents.values())
    )

Execute the explicit raw discovery adapter across all agents.

async def list_products(self,
request: ListProductsRequest) ‑> list[TaskResult[Union[ListProductsResponse1, ListProductsResponse2]]]
Expand source code
async def list_products(
    self, request: ListProductsRequest
) -> list[TaskResult[ListProductsResponse]]:
    """Execute compact product discovery across all agents."""
    import asyncio

    return await asyncio.gather(
        *(agent.list_products(request) for agent in self.agents.values())
    )

Execute compact product discovery across all agents.

async def refine_proposals(self,
request: RefineProposalsRequest) ‑> list[TaskResult[Union[RefineProposalsResponse1, RefineProposalsResponse2]]]
Expand source code
async def refine_proposals(
    self, request: RefineProposalsRequest
) -> list[TaskResult[RefineProposalsResponse]]:
    """Refine proposals across all agents."""
    import asyncio

    return await asyncio.gather(
        *(agent.refine_proposals(request) for agent in self.agents.values())
    )

Refine proposals across all agents.

async def request_proposals(self,
request: RequestProposalsRequest) ‑> list[TaskResult[Union[RequestProposalsResponse1, RequestProposalsResponse2, RequestProposalsResponse3, RequestProposalsResponse4]]]
Expand source code
async def request_proposals(
    self, request: RequestProposalsRequest
) -> list[TaskResult[RequestProposalsResponse]]:
    """Request proposals from all agents."""
    import asyncio

    return await asyncio.gather(
        *(agent.request_proposals(request) for agent in self.agents.values())
    )

Request proposals from all agents.

class ADCPProtocolError (message: str, agent_id: str | None = None, protocol: str | None = None)
Expand source code
class ADCPProtocolError(ADCPError):
    """Protocol-level error (malformed response, unexpected format)."""

    def __init__(self, message: str, agent_id: str | None = None, protocol: str | None = None):
        """Initialize protocol error."""
        suggestion = (
            f"The agent returned an unexpected {protocol} response format."
            if protocol
            else "Unexpected response format."
        )
        suggestion += "\n     Enable debug mode to see the full request/response."
        super().__init__(message, agent_id, None, suggestion)

Protocol-level error (malformed response, unexpected format).

Initialize protocol error.

Ancestors

  • ADCPError
  • builtins.Exception
  • builtins.BaseException

Inherited members

class ADCPSigningRequiredError (operation: str, agent_id: str | None = None, agent_uri: str | None = None)
Expand source code
class ADCPSigningRequiredError(ADCPError):
    """Raised when an operation in the seller's ``request_signing.required_for``
    is called without a ``SigningConfig`` on the client.

    Signing a ``required_for`` operation is mandatory — sending it unsigned
    would produce a ``request_signature_required`` rejection from the seller.
    Raising locally before the wire call saves a round-trip and gives the
    caller a clear, actionable error.
    """

    def __init__(
        self,
        operation: str,
        agent_id: str | None = None,
        agent_uri: str | None = None,
    ):
        self.operation = operation
        message = (
            f"Operation {operation!r} is in the seller's request_signing.required_for "
            f"list; signing is mandatory but no SigningConfig was provided"
        )
        suggestion = (
            "Pass signing=SigningConfig(private_key=..., key_id=...) when "
            "constructing ADCPClient. See adcp-keygen for key generation."
        )
        super().__init__(message, agent_id, agent_uri, suggestion)

Raised when an operation in the seller's request_signing.required_for is called without a SigningConfig on the client.

Signing a required_for operation is mandatory — sending it unsigned would produce a request_signature_required rejection from the seller. Raising locally before the wire call saves a round-trip and gives the caller a clear, actionable error.

Initialize exception with context.

Ancestors

  • ADCPError
  • builtins.Exception
  • builtins.BaseException

Inherited members

class ADCPTaskError (operation: str, errors: list[Any], agent_id: str | None = None)
Expand source code
class ADCPTaskError(ADCPError):
    """A task returned an ADCP error response.

    Provides structured access to the error objects from the response,
    including error codes for programmatic handling.
    """

    def __init__(
        self,
        operation: str,
        errors: list[Any],
        agent_id: str | None = None,
    ):
        """Initialize task error.

        Args:
            operation: The task that failed (e.g., "create_media_buy")
            errors: List of ADCP Error objects from the response
            agent_id: Optional agent ID for context
        """
        self.operation = operation
        self.errors = errors
        self.error_codes = [e.code for e in errors if hasattr(e, "code") and e.code]

        message = f"{operation} failed"
        if errors:
            first_msg = getattr(errors[0], "message", str(errors[0]))
            message = f"{operation} failed: {first_msg}"
            if len(errors) > 1:
                message += f" (+{len(errors) - 1} more)"

        super().__init__(message, agent_id=agent_id)

    @property
    def is_retryable(self) -> bool:
        """True if any error code is transient (RATE_LIMITED, etc.)."""
        from adcp.server.helpers import TRANSIENT_CODES

        return bool(TRANSIENT_CODES & set(self.error_codes))

A task returned an ADCP error response.

Provides structured access to the error objects from the response, including error codes for programmatic handling.

Initialize task error.

Args
-----=
operation
The task that failed (e.g., "create_media_buy")
errors
List of ADCP Error objects from the response
agent_id
Optional agent ID for context

Ancestors

  • ADCPError
  • builtins.Exception
  • builtins.BaseException

Subclasses

Instance variables

prop is_retryable : bool
Expand source code
@property
def is_retryable(self) -> bool:
    """True if any error code is transient (RATE_LIMITED, etc.)."""
    from adcp.server.helpers import TRANSIENT_CODES

    return bool(TRANSIENT_CODES & set(self.error_codes))

True if any error code is transient (RATE_LIMITED, etc.).

class ADCPTimeoutError (message: str,
agent_id: str | None = None,
agent_uri: str | None = None,
timeout: float | None = None)
Expand source code
class ADCPTimeoutError(ADCPError):
    """Request timed out."""

    def __init__(
        self,
        message: str,
        agent_id: str | None = None,
        agent_uri: str | None = None,
        timeout: float | None = None,
    ):
        """Initialize timeout error."""
        suggestion = (
            f"The request took longer than {timeout}s." if timeout else "The request timed out."
        )
        suggestion += "\n     Try increasing the timeout value or check if the agent is overloaded."
        super().__init__(message, agent_id, agent_uri, suggestion)

    @property
    def is_retryable(self) -> bool:
        return True

Request timed out.

Initialize timeout error.

Ancestors

  • ADCPError
  • builtins.Exception
  • builtins.BaseException

Inherited members

class ADCPToolNotFoundError (tool_name: str,
agent_id: str | None = None,
available_tools: list[str] | None = None)
Expand source code
class ADCPToolNotFoundError(ADCPError):
    """Requested tool not found on agent."""

    def __init__(
        self, tool_name: str, agent_id: str | None = None, available_tools: list[str] | None = None
    ):
        """Initialize tool not found error."""
        message = f"Tool '{tool_name}' not found on agent"
        suggestion = "List available tools with: python -m adcp list-tools --config <agent-id>"
        if available_tools:
            tools_list = ", ".join(available_tools[:5])
            if len(available_tools) > 5:
                tools_list += f", ... ({len(available_tools)} total)"
            suggestion = f"Available tools: {tools_list}"
        super().__init__(message, agent_id, None, suggestion)

Requested tool not found on agent.

Initialize tool not found error.

Ancestors

  • ADCPError
  • builtins.Exception
  • builtins.BaseException

Inherited members

class ADCPWebhookError (message: str,
agent_id: str | None = None,
agent_uri: str | None = None,
suggestion: str | None = None)
Expand source code
class ADCPWebhookError(ADCPError):
    """Webhook handling error."""

Webhook handling error.

Initialize exception with context.

Ancestors

  • ADCPError
  • builtins.Exception
  • builtins.BaseException

Subclasses

Inherited members

class ADCPWebhookSignatureError (message: str = 'Invalid webhook signature', agent_id: str | None = None)
Expand source code
class ADCPWebhookSignatureError(ADCPWebhookError):
    """Webhook signature verification failed."""

    def __init__(self, message: str = "Invalid webhook signature", agent_id: str | None = None):
        """Initialize webhook signature error."""
        suggestion = (
            "Verify that the webhook_secret matches the secret configured on the agent.\n"
            "     Webhook signatures use HMAC-SHA256 for security."
        )
        super().__init__(message, agent_id, None, suggestion)

Webhook signature verification failed.

Initialize webhook signature error.

Ancestors

Inherited members

class AcceptProposalRequest (**data: Any)
Expand source code
class AcceptProposalRequest(AdCPBaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    adcp_version: version_envelope.AdcpVersion | None = None
    adcp_major_version: version_envelope.AdcpMajorVersion | None = None
    idempotency_key: Annotated[
        str, Field(max_length=255, min_length=16, pattern='^[A-Za-z0-9_.:-]{16,255}$')
    ]
    account: canonical_account_ref.CanonicalAccountReference
    proposal_id: Annotated[str, Field(min_length=1)]
    proposal_terms_digest: Annotated[
        str,
        Field(
            description='terms_digest from the committed proposal. The seller MUST atomically verify both ID and digest before acceptance.',
            pattern='^sha256:[A-Za-z0-9_-]{43}$',
        ),
    ]
    total_budget: Annotated[
        TotalBudget | None,
        Field(
            description='Execution amount when the committed proposal defines scalable percentages or constraints rather than a fixed total.'
        ),
    ] = None
    daily_budget_cap: Annotated[
        float | None,
        Field(
            description="Optional hard aggregate daily spend ceiling applied when the committed proposal is accepted. It constrains execution without changing the proposal's negotiated pricing.",
            ge=0.0,
        ),
    ] = None
    budget_cap_timezone: Annotated[
        str | None,
        Field(
            description='Optional shared IANA cap-day timezone override. Requires buyer_timezone_override support. When omitted, budget_capping.timezone_basis selects Account.timezone or fixed_timezone.',
            min_length=1,
        ),
    ] = None
    io_acceptance: IoAcceptance | None = None
    purchase_order_ref: Annotated[str | None, Field(max_length=255, min_length=1)] = None
    governance_context: Annotated[str | None, Field(max_length=4096, min_length=1)] = None
    push_notification_config: push_notification_config_1.PushNotificationConfig | None = None
    reporting_webhook: Annotated[
        reporting_webhook_1.ReportingWebhook | None,
        Field(
            description='Optional reporting delivery configuration established atomically when the proposal is accepted. This is execution metadata and does not alter the accepted commercial terms digest.'
        ),
    ] = None
    opportunity: Annotated[
        Opportunity | None,
        Field(
            description='Optional planning-cycle closure. Success infers closed with accepted_with_seller when status is omitted. If the proposal carries opportunity_id, a supplied ID MUST match; the accepted proposal preserves that association.'
        ),
    ] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var account : adcp.types.generated_poc.core.canonical_account_ref.CanonicalAccountReference
var adcp_major_version : adcp.types.generated_poc.core.version_envelope.AdcpMajorVersion | None
var adcp_version : adcp.types.generated_poc.core.version_envelope.AdcpVersion | None
var budget_cap_timezone : str | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var daily_budget_cap : float | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var governance_context : str | None
var idempotency_key : str
var io_acceptance : adcp.types.generated_poc.media_buy.accept_proposal_request.IoAcceptance | None
var model_config
var opportunity : adcp.types.generated_poc.media_buy.accept_proposal_request.Opportunity | None
var proposal_id : str
var proposal_terms_digest : str
var purchase_order_ref : str | None
var push_notification_config : adcp.types.generated_poc.core.push_notification_config.PushNotificationConfig | None
var reporting_webhook : adcp.types.generated_poc.core.reporting_webhook.ReportingWebhook | None
var total_budget : adcp.types.generated_poc.media_buy.accept_proposal_request.TotalBudget | None

Inherited members

class AcceptedLoss (*args, **kwds)
Expand source code
class AcceptedLoss(StrEnum):
    feed_version_not_atomic = 'feed_version_not_atomic'
    pricing_version_not_atomic = 'pricing_version_not_atomic'
    mutation_idempotency_not_guaranteed = 'mutation_idempotency_not_guaranteed'

Enum where members are also (and must be) strings

Ancestors

  • enum.StrEnum
  • builtins.str
  • enum.ReprEnum
  • enum.Enum

Class variables

var feed_version_not_atomic
var mutation_idempotency_not_guaranteed
var pricing_version_not_atomic
class AccountAuthorization (**data: Any)
Expand source code
class AccountAuthorization(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    allowed_tasks: Annotated[
        list[AllowedTask],
        Field(
            description='Canonical snake_case task names the caller may invoke against this account (for example get_media_buys, buy_products, accept_proposal, control_media_buy, or sync_creatives). Absence of a task from this list means not permitted and returns SCOPE_INSUFFICIENT. Compact 3.2 tools are authorized by their own names; grants for deprecated get_products/create_media_buy/update_media_buy do not silently transfer across aliases.'
        ),
    ]
    field_scopes: Annotated[
        dict[str, list[str]] | None,
        Field(
            description='Optional per-task allowlist of request fields the caller may set. Keys are task names (which MUST also appear in allowed_tasks). Values are arrays of top-level request-field paths permitted for that task. When a task appears in field_scopes, requests to that task with any field outside the allowlist MUST be rejected with FIELD_NOT_PERMITTED. Compact product tools use their own task names and top-level fields such as criteria and refinements. Implicit framing fields are always permitted and do NOT need to appear in the allowlist — they identify the resource or shape the call rather than mutating business state. Tasks absent from field_scopes have no field-level restriction beyond what the task schema already enforces.'
        ),
    ] = None
    scope_name: Annotated[
        Literal['attestation_verifier'] | ScopeName | None,
        Field(
            description='Optional named scope identifier. When present, callers and the vendor agent can reason about the grant by name rather than by enumerating allowed_tasks and field_scopes. Modeled as a discriminated union so code generators produce a literal type for the standard scope(s) and a distinct type for agent-defined values — this prevents a typo of `attestation_verifier` from being silently accepted as a custom scope. Agent-defined scope names MUST use a `custom:` prefix to avoid collision with future standard scopes. The prefix is protocol-neutral: a signals agent, a governance agent, or a creative agent defines custom scopes the same way a media-buy sales agent does.'
        ),
    ] = None
    read_only: Annotated[
        bool | None,
        Field(
            description='Convenience flag. When true, the caller is permitted only non-mutating tasks. Sellers MUST reject any mutation from a read-only caller with READ_ONLY_SCOPE. For the AdCP 3.2 product split, read-only permits list_products but rejects request_proposals, refine_proposals, and decline_proposals; a legacy get_products call is permitted only when the seller can guarantee the selected arm is a synchronous side-effect-free read. Sellers MAY omit this field; omission is equivalent to `false`. Callers MUST NOT infer read-only from `allowed_tasks` alone — the seller MUST set this explicitly when it applies.'
        ),
    ] = False

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var allowed_tasks : list[adcp.types.generated_poc.core.account_authorization.AllowedTask]
var field_scopes : dict[str, list[str]] | None
var model_config
var read_only : bool | None
var scope_name : Literal['attestation_verifier'] | adcp.types.generated_poc.core.account_authorization.ScopeName | None

Inherited members

class AccountReference (root: RootModelRootType = PydanticUndefined, **data)
Expand source code
class AccountReference(RootModel[AccountReference1 | AccountReference2]):
    root: Annotated[
        AccountReference1 | AccountReference2,
        Field(
            description="Reference to an advertiser account by seller-assigned ID or natural key. account_id is the seller/storefront handle returned by list_accounts. The natural key is brand + operator + optional operator_unit.id + optional fixed currency + optional buyer-selected account timezone + sandbox; it describes the advertiser object the seller resolves or provisions. After a capability-gated identity rekey, the former natural key is tombstoned and returns ACCOUNT_MOVED to authorized callers rather than resolving or provisioning another account. brand.countries qualifies the advertiser's geographic identity. operator_unit identifies the operator-owned buying seat or business unit and is distinct from the seller account. operator_unit.name is display metadata and does not participate in identity. Use account_id when the seller or upstream platform owns the canonical account namespace; use the natural key for buyer-declared accounts (require_operator_auth: false).",
            examples=[
                {'account_id': 'acc_acme_001'},
                {'brand': {'domain': 'acme-corp.com'}, 'operator': 'acme-corp.com'},
                {
                    'brand': {'domain': 'nova-brands.com', 'brand_id': 'spark'},
                    'operator': 'pinnacle-media.com',
                },
                {
                    'brand': {'domain': 'nova-athletics.example', 'countries': ['NL']},
                    'operator': 'nova-athletics.example',
                    'operator_unit': {'id': '234284238', 'name': 'Nova EMEA'},
                    'currency': 'EUR',
                    'timezone': 'Europe/Amsterdam',
                },
                {
                    'brand': {'domain': 'acme-corp.com'},
                    'operator': 'acme-corp.com',
                    'sandbox': True,
                },
            ],
            title='Account Reference',
        ),
    ]
    def __getattr__(self, name: str) -> Any:
        """Proxy attribute access to the wrapped type."""
        if name.startswith('_'):
            raise AttributeError(name)
        return getattr(self.root, name)

Usage Documentation

RootModel and Custom Root Types

A Pydantic BaseModel for the root object of the model.

Attributes
-----=
root
The root object of the model.
__pydantic_root_model__
Whether the model is a RootModel.
__pydantic_private__
Private fields in the model.
__pydantic_extra__
Extra fields in the model.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • pydantic.root_model.RootModel[Union[AccountReference1, AccountReference2]]
  • pydantic.root_model.RootModel
  • pydantic.main.BaseModel
  • typing.Generic

Class variables

var model_config
var root : adcp.types.generated_poc.core.account_ref.AccountReference1 | adcp.types.generated_poc.core.account_ref.AccountReference2
class AccountReferenceById (**data: Any)
Expand source code
class AccountReference1(AdCPBaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    account_id: Annotated[
        str,
        Field(
            description="Seller-assigned account identifier. For upstream-managed account namespaces, this value comes from list_accounts; for seller-defined namespaces without a list_accounts surface, it is supplied out-of-band. Buyer-declared account sellers MAY echo account_id from sync_accounts as an internal handle, but they MUST continue accepting the account's current natural-key AccountRef on subsequent calls. A former key tombstoned by identity reconciliation returns ACCOUNT_MOVED to authorized callers."
        ),
    ]

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var account_id : str
var model_config

Inherited members

class AccountReferenceByNaturalKey (**data: Any)
Expand source code
class AccountReference2(AdCPBaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    brand: Annotated[
        brand_ref.BrandReference, Field(description='Brand reference identifying the advertiser')
    ]
    operator: Annotated[
        str,
        Field(
            description="Domain of the entity operating on the brand's behalf. When the brand operates directly, this is the brand's domain.",
            pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$',
        ),
    ]
    operator_unit: Annotated[
        operator_unit_1.OperatorUnit | None,
        Field(
            description='Optional operator-owned business unit, agency seat, or platform account. Only id participates in the natural account key; name is mutable display metadata.'
        ),
    ] = None
    currency: Annotated[
        str | None,
        Field(
            description="Immutable ISO 4217 transaction currency when the seller's advertiser object is currency-bound. When present, this is part of the natural account key and media buys on the account MUST use it. Omit when currency is selected independently per media buy.",
            pattern='^[A-Z]{3}$',
        ),
    ] = None
    timezone: Annotated[
        str | None,
        Field(
            description='Immutable account timezone. Include it in the natural key when get_adcp_capabilities.account.timezone declares account_fixed with buyer_selected; omit it for seller_fixed or seller_assigned accounts.',
            min_length=1,
        ),
    ] = None
    sandbox: Annotated[
        bool | None,
        Field(
            description='When true, references the sandbox account for this brand/operator pair. Defaults to false (production account).'
        ),
    ] = False

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var brand : adcp.types.generated_poc.core.brand_ref.BrandReference
var currency : str | None
var model_config
var operator : str
var operator_unit : adcp.types.generated_poc.core.operator_unit.OperatorUnit | None
var sandbox : bool | None
var timezone : str | None

Inherited members

class AccountScope (*args, **kwds)
Expand source code
class AccountScope(StrEnum):
    operator = 'operator'
    brand = 'brand'
    operator_brand = 'operator_brand'
    agent = 'agent'

Enum where members are also (and must be) strings

Ancestors

  • enum.StrEnum
  • builtins.str
  • enum.ReprEnum
  • enum.Enum

Class variables

var agent
var brand
var operator
var operator_brand
class AccountWithAuthorization (**data: Any)
Expand source code
class AccountWithAuthorization(Account):
    authorization: Annotated[
        account_authorization.AccountAuthorization | None,
        Field(
            description="Optional. The caller's scope grant against this account. Vendor agents of any type (media-buy, signals, governance, creative, brand) that support scope introspection SHOULD populate this so callers can preempt SCOPE_INSUFFICIENT / FIELD_NOT_PERMITTED errors rather than discovering scope by trial and error. Media-buy sales agents claiming the `attestation_verifier` standard scope MUST populate it. Absence means the vendor agent does not advertise introspectable scope for this account — callers MUST NOT infer access from absence, and fall back to error-driven discovery via the RBAC error codes."
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.account.Account
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var authorization : adcp.types.generated_poc.core.account_authorization.AccountAuthorization | None
var model_config

Inherited members

class AcquireRightsRequest (**data: Any)
Expand source code
class AcquireRightsRequest(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    governance_context: Annotated[
        str | None,
        Field(
            description='Opaque intent authorization for this rights commitment. Required when governance applies to the resolved account.',
            max_length=4096,
            min_length=1,
            pattern='^[\\x20-\\x7E]+$',
        ),
    ] = None
    rights_id: Annotated[
        str, Field(description='Rights offering identifier from get_rights response')
    ]
    pricing_option_id: Annotated[
        str, Field(description='Selected pricing option from the rights offering')
    ]
    buyer: Annotated[brand_ref.BrandReference, Field(description="The buyer's brand identity")]
    account: Annotated[
        account_ref.AccountReference | None,
        Field(
            description='Account context for this acquisition. Used by the brand agent to resolve any governance agent previously bound for this brand+operator pair via sync_governance. When both an inline governance_context token and a bound governance agent are present, the token is verified against that resolved relationship. An agent advertising adcp.governance_enforcement for acquire_rights MUST resolve an account and returns ACCOUNT_REQUIRED when neither the request nor an existing resource supplies one; it MUST NOT infer that a missing token means ungoverned. Legacy non-claiming agents may continue to treat omission of both fields as ungoverned during 3.x. Pass a natural key (brand, operator, optional sandbox) or a seller-assigned account_id from list_accounts.'
        ),
    ] = None
    campaign: Annotated[Campaign, Field(description='Campaign details for rights clearance')]
    revocation_webhook: Annotated[
        push_notification_config_1.PushNotificationConfig,
        Field(
            description='Webhook for rights revocation notifications. If the rights holder needs to revoke rights (talent scandal, contract violation, etc.), they POST a revocation-notification to this URL. The buyer is responsible for stopping creative delivery upon receipt.'
        ),
    ]
    push_notification_config: Annotated[
        push_notification_config_1.PushNotificationConfig | None,
        Field(
            description='Webhook for async status updates if the acquisition requires approval. The rights agent sends a webhook notification when the status transitions to acquired or rejected.'
        ),
    ] = None
    idempotency_key: Annotated[
        str,
        Field(
            description='Client-generated key for safe retries. Resubmitting with the same key returns the original response rather than creating a duplicate acquisition. MUST be unique per (seller, request) pair to prevent cross-seller correlation. Use a fresh UUID v4 for each request.',
            max_length=255,
            min_length=16,
            pattern='^[A-Za-z0-9_.:-]{16,255}$',
        ),
    ]
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var account : adcp.types.generated_poc.core.account_ref.AccountReference | None
var buyer : adcp.types.generated_poc.core.brand_ref.BrandReference
var campaign : adcp.types.generated_poc.brand.acquire_rights_request.Campaign
var context : adcp.types.generated_poc.core.context.ContextObject | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var governance_context : str | None
var idempotency_key : str
var model_config
var pricing_option_id : str
var push_notification_config : adcp.types.generated_poc.core.push_notification_config.PushNotificationConfig | None
var revocation_webhook : adcp.types.generated_poc.core.push_notification_config.PushNotificationConfig
var rights_id : str

Inherited members

class AcquireRightsResponse1 (**data: Any)
Expand source code
class AcquireRightsResponse1(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    rights_id: str
    rights_status: Literal['acquired'] = 'acquired'
    brand_id: str
    terms: rights_terms_1.RightsTerms
    generation_credentials: list[generation_credential_1.GenerationCredential]
    restrictions: list[str] | None = None
    disclosure: Disclosure | None = None
    approval_webhook: push_notification_config_1.PushNotificationConfig | None = None
    usage_reporting_url: AnyUrl | None = None
    rights_constraint: Any
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var approval_webhook : adcp.types.generated_poc.core.push_notification_config.PushNotificationConfig | None
var brand_id : str
var context : adcp.types.generated_poc.core.context.ContextObject | None
var disclosure : adcp.types.generated_poc.brand.acquire_rights_response.Disclosure | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var generation_credentials : list[adcp.types.generated_poc.core.generation_credential.GenerationCredential]
var model_config
var restrictions : list[str] | None
var rights_constraint : Any
var rights_id : str
var rights_status : Literal['acquired']
var terms : adcp.types.generated_poc.brand.rights_terms.RightsTerms
var usage_reporting_url : pydantic.networks.AnyUrl | None
class AcquireRightsAcquiredResponse (**data: Any)
Expand source code
class AcquireRightsResponse1(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    rights_id: str
    rights_status: Literal['acquired'] = 'acquired'
    brand_id: str
    terms: rights_terms_1.RightsTerms
    generation_credentials: list[generation_credential_1.GenerationCredential]
    restrictions: list[str] | None = None
    disclosure: Disclosure | None = None
    approval_webhook: push_notification_config_1.PushNotificationConfig | None = None
    usage_reporting_url: AnyUrl | None = None
    rights_constraint: Any
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var approval_webhook : adcp.types.generated_poc.core.push_notification_config.PushNotificationConfig | None
var brand_id : str
var context : adcp.types.generated_poc.core.context.ContextObject | None
var disclosure : adcp.types.generated_poc.brand.acquire_rights_response.Disclosure | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var generation_credentials : list[adcp.types.generated_poc.core.generation_credential.GenerationCredential]
var model_config
var restrictions : list[str] | None
var rights_constraint : Any
var rights_id : str
var rights_status : Literal['acquired']
var terms : adcp.types.generated_poc.brand.rights_terms.RightsTerms
var usage_reporting_url : pydantic.networks.AnyUrl | None

Inherited members

class AcquireRightsPendingResponse (**data: Any)
Expand source code
class AcquireRightsResponse2(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    rights_id: str
    rights_status: Literal['pending_approval'] = 'pending_approval'
    brand_id: str
    detail: str | None = None
    estimated_response_time: str | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var brand_id : str
var context : adcp.types.generated_poc.core.context.ContextObject | None
var detail : str | None
var estimated_response_time : str | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config
var rights_id : str
var rights_status : Literal['pending_approval']

Inherited members

class AcquireRightsRejectedResponse (**data: Any)
Expand source code
class AcquireRightsResponse3(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    rights_id: str
    rights_status: Literal['rejected'] = 'rejected'
    brand_id: str
    reason: str
    suggestions: list[str] | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var brand_id : str
var context : adcp.types.generated_poc.core.context.ContextObject | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config
var reason : str
var rights_id : str
var rights_status : Literal['rejected']
var suggestions : list[str] | None

Inherited members

class AcquireRightsErrorResponse (**data: Any)
Expand source code
class AcquireRightsResponse4(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    errors: Annotated[list[error_1.Error], Field(min_length=1)]
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var errors : list[adcp.types.generated_poc.core.error.Error]
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config

Inherited members

class ActivateSignalRequest (**data: Any)
Expand source code
class ActivateSignalRequest(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    action: Annotated[
        Action | None,
        Field(
            description="Whether to activate or deactivate the signal. Deactivating removes the segment from downstream platforms, required when campaigns end to comply with data governance policies (GDPR, CCPA). Defaults to 'activate' when omitted."
        ),
    ] = Action.activate
    signal_agent_segment_id: Annotated[
        str,
        Field(
            description='Opaque activation handle returned in the signal_agent_segment_id field of each get_signals response entry. Pass this string verbatim — do not pass the signal_id object.'
        ),
    ]
    destinations: Annotated[
        list[destination.Destination],
        Field(
            description='Target destination(s) for activation. If the authenticated caller matches one of these destinations, activation keys will be included in the response.',
            min_length=1,
        ),
    ]
    pricing_option_id: Annotated[
        str | None,
        Field(
            description="The pricing option selected from the signal's pricing_options in the get_signals response. Required when the signal has pricing options. Records the buyer's pricing commitment at activation time; pass this same value in report_usage for billing verification."
        ),
    ] = None
    governance_context: Annotated[
        str | None,
        Field(
            description='Opaque authorization context returned by an approved check_governance decision for this signal activation. Required when the account has a registered governance agent; signal agents MUST reject governed activations that omit a valid context. Conditions and denied decisions never produce this value.',
            max_length=4096,
            min_length=1,
            pattern='^[\\x20-\\x7E]+$',
        ),
    ] = None
    account: Annotated[
        account_ref.AccountReference | None,
        Field(
            description='Account for this activation. Associates with a commercial relationship established via sync_accounts.'
        ),
    ] = None
    idempotency_key: Annotated[
        str,
        Field(
            description='Client-generated unique key for this request. Prevents duplicate activations on retries. MUST be unique per (seller, request) pair to prevent cross-seller correlation. Use a fresh UUID v4 for each request.',
            max_length=255,
            min_length=16,
            pattern='^[A-Za-z0-9_.:-]{16,255}$',
        ),
    ]
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var account : adcp.types.generated_poc.core.account_ref.AccountReference | None
var action : adcp.types.generated_poc.signals.activate_signal_request.Action | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var destinations : list[adcp.types.generated_poc.core.destination.Destination]
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var governance_context : str | None
var idempotency_key : str
var model_config
var pricing_option_id : str | None
var signal_agent_segment_id : str

Inherited members

class ActivateSignalResponse1 (**data: Any)
Expand source code
class ActivateSignalResponse1(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    deployments: list[deployment_1.Deployment]
    sandbox: bool | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var deployments : list[adcp.types.generated_poc.core.deployment.Deployment]
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config
var sandbox : bool | None
class ActivateSignalSuccessResponse (**data: Any)
Expand source code
class ActivateSignalResponse1(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    deployments: list[deployment_1.Deployment]
    sandbox: bool | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var deployments : list[adcp.types.generated_poc.core.deployment.Deployment]
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config
var sandbox : bool | None

Inherited members

class ActivateSignalErrorResponse (**data: Any)
Expand source code
class ActivateSignalResponse2(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    errors: Annotated[list[error_1.Error], Field(min_length=1)]
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var errors : list[adcp.types.generated_poc.core.error.Error]
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config

Inherited members

class SegmentIdActivationKey (**data: Any)
Expand source code
class ActivationKey1(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    type: Annotated[Literal['segment_id'], Field(description='Segment ID based targeting')] = 'segment_id'
    segment_id: Annotated[
        str,
        Field(description='The platform-specific segment identifier to use in campaign targeting'),
    ]

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var model_config
var segment_id : str
var type : Literal['segment_id']
class PropertyIdActivationKey (**data: Any)
Expand source code
class ActivationKey1(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    type: Annotated[Literal['segment_id'], Field(description='Segment ID based targeting')] = 'segment_id'
    segment_id: Annotated[
        str,
        Field(description='The platform-specific segment identifier to use in campaign targeting'),
    ]

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var model_config
var segment_id : str
var type : Literal['segment_id']

Inherited members

class KeyValueActivationKey (**data: Any)
Expand source code
class ActivationKey2(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    type: Annotated[Literal['key_value'], Field(description='Key-value pair based targeting')] = 'key_value'
    key: Annotated[str, Field(description='The targeting parameter key')]
    value: Annotated[str, Field(description='The targeting parameter value')]

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var key : str
var model_config
var type : Literal['key_value']
var value : str
class PropertyTagActivationKey (**data: Any)
Expand source code
class ActivationKey2(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    type: Annotated[Literal['key_value'], Field(description='Key-value pair based targeting')] = 'key_value'
    key: Annotated[str, Field(description='The targeting parameter key')]
    value: Annotated[str, Field(description='The targeting parameter value')]

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var key : str
var model_config
var type : Literal['key_value']
var value : str

Inherited members

class AdAgentsValidationResult (domain: str,
url: str,
discovery_method: DiscoveryMethod = 'direct',
manager_domain: str | None = None,
data: dict[str, Any] | None = None,
valid: bool = False,
errors: list[str] = <factory>)
Expand source code
@dataclass
class AdAgentsValidationResult:
    """Result of discovering and validating a publisher's adagents.json.

    ``discovery_method`` records which path produced ``data``:
    ``direct`` for ``/.well-known/adagents.json`` on the publisher,
    ``authoritative_location`` for a URL-reference redirect, and
    ``ads_txt_managerdomain`` for the one-hop ads.txt MANAGERDOMAIN
    fallback (RFC 4175). ``manager_domain`` is set only on the
    managerdomain path.
    """

    domain: str
    url: str
    discovery_method: DiscoveryMethod = "direct"
    manager_domain: str | None = None
    data: dict[str, Any] | None = None
    valid: bool = False
    errors: list[str] = field(default_factory=list)

Result of discovering and validating a publisher's adagents.json.

discovery_method records which path produced data: direct for /.well-known/adagents.json on the publisher, authoritative_location for a URL-reference redirect, and ads_txt_managerdomain for the one-hop ads.txt MANAGERDOMAIN fallback (RFC 4175). manager_domain is set only on the managerdomain path.

Instance variables

var data : dict[str, typing.Any] | None
var discovery_method : Literal['direct', 'authoritative_location', 'ads_txt_managerdomain']
var domain : str
var errors : list[str]
var manager_domain : str | None
var url : str
var valid : bool
class AdagentsAccessBlockedError (publisher_domain: str)
Expand source code
class AdagentsAccessBlockedError(AdagentsValidationError):
    """adagents.json fetch blocked by publisher-side bot management (403, cf-mitigated: challenge).

    Only surfaces in direct-fetch workflows (``fetch_adagents``). SDK callers that
    use ``fetch_agent_authorizations`` avoid this entirely — the AAO directory crawler
    handles publisher fetches and serves cached results without exposing the SDK to
    publisher-side bot management.

    If you need to catch this specifically without catching all
    ``AdagentsValidationError``s, use ``except AdagentsAccessBlockedError``.
    """

    def __init__(self, publisher_domain: str):
        """Initialize bot-management blocked error."""
        self.publisher_domain = publisher_domain
        message = (
            f"adagents.json blocked by bot management for {publisher_domain} "
            f"(HTTP 403, cf-mitigated: challenge)"
        )
        suggestion = (
            "The publisher's origin blocked this request with a Cloudflare bot management\n"
            "     challenge. This only affects direct adagents.json fetches (fetch_adagents).\n"
            "\n"
            "     To unblock local debugging:\n"
            "     - Retry with a browser-like User-Agent via the user_agent= parameter, e.g.\n"
            '       user_agent="Mozilla/5.0"\n'
            "     - Or call fetch_agent_authorizations() to query the AAO directory instead,\n"
            "       which bypasses publisher-side bot management entirely."
        )
        super().__init__(message, None, None, suggestion)

adagents.json fetch blocked by publisher-side bot management (403, cf-mitigated: challenge).

Only surfaces in direct-fetch workflows (fetch_adagents()). SDK callers that use fetch_agent_authorizations() avoid this entirely — the AAO directory crawler handles publisher fetches and serves cached results without exposing the SDK to publisher-side bot management.

If you need to catch this specifically without catching all AdagentsValidationErrors, use except AdagentsAccessBlockedError.

Initialize bot-management blocked error.

Ancestors

Inherited members

class AdagentsCacheEntry (body: dict[str, Any], etag: str | None = None, last_modified: str | None = None)
Expand source code
@dataclass(frozen=True)
class AdagentsCacheEntry:
    """Conditional-refresh cache state for an adagents.json URL.

    Pass an entry into :func:`fetch_adagents_with_cache` to send
    ``If-None-Match`` (preferred) and ``If-Modified-Since`` validators
    on the next fetch. A 304 from the publisher is treated as a
    successful cache-lifetime refresh — the ``body`` is returned
    unchanged with refreshed timing, per the adcp#4504 fetch contract.
    """

    body: dict[str, Any]
    etag: str | None = None
    last_modified: str | None = None

Conditional-refresh cache state for an adagents.json URL.

Pass an entry into :func:fetch_adagents_with_cache() to send If-None-Match (preferred) and If-Modified-Since validators on the next fetch. A 304 from the publisher is treated as a successful cache-lifetime refresh — the body is returned unchanged with refreshed timing, per the adcp#4504 fetch contract.

Instance variables

var body : dict[str, typing.Any]
var etag : str | None
var last_modified : str | None
class AdagentsEntryError (index: int, kind: EntryErrorKind, message: str, url: str | None = None)
Expand source code
@dataclass(frozen=True)
class AdagentsEntryError:
    """A single schema violation found in an adagents.json file.

    ``kind`` is a stable string literal callers can branch on (e.g.,
    distinguish a publisher who shipped bare entries from one who picked
    an unknown authorization_type). ``message`` is developer-facing and
    its wording may change between releases — pattern-match on ``kind``
    when surfacing publisher-facing diagnostics.

    For file-level errors (e.g., an empty authorization and catalog file)
    ``index`` is ``-1`` and ``url`` is ``None``.
    """

    index: int
    kind: EntryErrorKind
    message: str
    url: str | None = None

A single schema violation found in an adagents.json file.

kind is a stable string literal callers can branch on (e.g., distinguish a publisher who shipped bare entries from one who picked an unknown authorization_type). message is developer-facing and its wording may change between releases — pattern-match on kind when surfacing publisher-facing diagnostics.

For file-level errors (e.g., an empty authorization and catalog file) index is -1 and url is None.

Instance variables

var index : int
var kind : Literal['missing_authorized_agents', 'missing_url', 'missing_authorized_for', 'missing_authorization_type', 'unknown_authorization_type', 'missing_selector_for_type', 'not_an_object', 'empty_authorized_agents', 'reference_renderer_missing_catalog_metadata', 'invalid_reference_renderer', 'reference_renderer_revision_mismatch', 'reference_renderer_untrusted_origin']
var message : str
var url : str | None
class AdagentsFetchResult (data: dict[str, Any],
discovery_method: DiscoveryMethod,
etag: str | None = None,
last_modified: str | None = None,
not_modified: bool = False)
Expand source code
@dataclass(frozen=True)
class AdagentsFetchResult:
    """Result of a fetch, including refreshed cache validators.

    ``not_modified`` is True when the server returned 304 and ``data``
    came from the supplied cache entry. ``etag`` / ``last_modified`` are
    the validators to persist for the next fetch — on 304 they come
    from the 304 response headers if present, falling back to the
    supplied entry's values.
    """

    data: dict[str, Any]
    discovery_method: DiscoveryMethod
    etag: str | None = None
    last_modified: str | None = None
    not_modified: bool = False

Result of a fetch, including refreshed cache validators.

not_modified is True when the server returned 304 and data came from the supplied cache entry. etag / last_modified are the validators to persist for the next fetch — on 304 they come from the 304 response headers if present, falling back to the supplied entry's values.

Instance variables

var data : dict[str, typing.Any]
var discovery_method : Literal['direct', 'authoritative_location', 'ads_txt_managerdomain']
var etag : str | None
var last_modified : str | None
var not_modified : bool
class AdagentsNotFoundError (publisher_domain: str)
Expand source code
class AdagentsNotFoundError(AdagentsValidationError):
    """adagents.json file not found (404)."""

    def __init__(self, publisher_domain: str):
        """Initialize not found error."""
        message = f"adagents.json not found for domain: {publisher_domain}"
        suggestion = (
            "Verify that the publisher has deployed adagents.json to:\n"
            f"     https://{publisher_domain}/.well-known/adagents.json"
        )
        super().__init__(message, None, None, suggestion)

adagents.json file not found (404).

Initialize not found error.

Ancestors

Inherited members

class AdagentsTimeoutError (publisher_domain: str, timeout: float)
Expand source code
class AdagentsTimeoutError(AdagentsValidationError):
    """Request for adagents.json timed out."""

    def __init__(self, publisher_domain: str, timeout: float):
        """Initialize timeout error."""
        message = f"Request to fetch adagents.json timed out after {timeout}s"
        suggestion = (
            "The publisher's server may be slow or unresponsive.\n"
            "     Try increasing the timeout value or check the domain is correct."
        )
        super().__init__(message, None, None, suggestion)

Request for adagents.json timed out.

Initialize timeout error.

Ancestors

Inherited members

class AdagentsValidationError (message: str,
agent_id: str | None = None,
agent_uri: str | None = None,
suggestion: str | None = None)
Expand source code
class AdagentsValidationError(ADCPError):
    """Base error for adagents.json validation issues."""

Base error for adagents.json validation issues.

Initialize exception with context.

Ancestors

  • ADCPError
  • builtins.Exception
  • builtins.BaseException

Subclasses

Inherited members

class AdagentsValidationReport (schema_valid: bool,
errors: list[AdagentsEntryError],
authorized_agents_count: int,
properties_count: int,
is_reference: bool = False)
Expand source code
@dataclass(frozen=True)
class AdagentsValidationReport:
    """Result of structurally validating a parsed adagents.json.

    Distinguishes the two failure modes that
    :func:`get_properties_by_agent` collapses into an empty list:
    a schema-invalid file (``schema_valid`` is False, ``errors`` populated)
    versus a valid file that simply doesn't list the caller's agent.

    ``authorized_agents_count`` and ``properties_count`` reflect the
    array lengths as observed in the input — they are reported regardless
    of ``schema_valid`` so callers can show "0 agents listed" diagnostics
    on partially-broken files.

    ``is_reference`` is True for the URL-reference variant of the schema
    (an ``authoritative_location`` pointer with no inline
    ``authorized_agents`` array). Callers that received a report with
    ``is_reference=True`` should follow the redirect (e.g., via
    :func:`fetch_adagents`) and validate the resolved file. This flag
    lets callers distinguish a legitimate URL-reference file from an inline
    catalog-only file. AdCP 3.2 permits ``authorized_agents: []`` when at least
    one of ``formats``, ``properties``, ``placements``, ``collections``, or
    ``signals`` is non-empty; that catalog content grants no sales authority.
    """

    schema_valid: bool
    errors: list[AdagentsEntryError]
    authorized_agents_count: int
    properties_count: int
    is_reference: bool = False

Result of structurally validating a parsed adagents.json.

Distinguishes the two failure modes that :func:get_properties_by_agent() collapses into an empty list: a schema-invalid file (schema_valid is False, errors populated) versus a valid file that simply doesn't list the caller's agent.

authorized_agents_count and properties_count reflect the array lengths as observed in the input — they are reported regardless of schema_valid so callers can show "0 agents listed" diagnostics on partially-broken files.

is_reference is True for the URL-reference variant of the schema (an authoritative_location pointer with no inline authorized_agents array). Callers that received a report with is_reference=True should follow the redirect (e.g., via :func:fetch_adagents()) and validate the resolved file. This flag lets callers distinguish a legitimate URL-reference file from an inline catalog-only file. AdCP 3.2 permits authorized_agents: [] when at least one of formats, properties, placements, collections, or signals is non-empty; that catalog content grants no sales authority.

Instance variables

var authorized_agents_count : int
var errors : list[AdagentsEntryError]
var is_reference : bool
var properties_count : int
var schema_valid : bool
class AdvertiserIndustry (*args, **kwds)
Expand source code
class AdvertiserIndustry(StrEnum):
    automotive = 'automotive'
    automotive_electric_vehicles = 'automotive.electric_vehicles'
    automotive_parts_accessories = 'automotive.parts_accessories'
    automotive_luxury = 'automotive.luxury'
    beauty_cosmetics = 'beauty_cosmetics'
    beauty_cosmetics_skincare = 'beauty_cosmetics.skincare'
    beauty_cosmetics_fragrance = 'beauty_cosmetics.fragrance'
    beauty_cosmetics_haircare = 'beauty_cosmetics.haircare'
    cannabis = 'cannabis'
    cpg = 'cpg'
    cpg_personal_care = 'cpg.personal_care'
    cpg_household = 'cpg.household'
    dating = 'dating'
    education = 'education'
    education_higher_education = 'education.higher_education'
    education_online_learning = 'education.online_learning'
    education_k12 = 'education.k12'
    energy_utilities = 'energy_utilities'
    energy_utilities_renewable = 'energy_utilities.renewable'
    fashion_apparel = 'fashion_apparel'
    fashion_apparel_luxury = 'fashion_apparel.luxury'
    fashion_apparel_sportswear = 'fashion_apparel.sportswear'
    finance = 'finance'
    finance_banking = 'finance.banking'
    finance_insurance = 'finance.insurance'
    finance_investment = 'finance.investment'
    finance_cryptocurrency = 'finance.cryptocurrency'
    food_beverage = 'food_beverage'
    food_beverage_alcohol = 'food_beverage.alcohol'
    food_beverage_restaurants = 'food_beverage.restaurants'
    food_beverage_packaged_goods = 'food_beverage.packaged_goods'
    gambling_betting = 'gambling_betting'
    gambling_betting_sports_betting = 'gambling_betting.sports_betting'
    gambling_betting_casino = 'gambling_betting.casino'
    gaming = 'gaming'
    gaming_mobile = 'gaming.mobile'
    gaming_console_pc = 'gaming.console_pc'
    gaming_esports = 'gaming.esports'
    government_nonprofit = 'government_nonprofit'
    government_nonprofit_political = 'government_nonprofit.political'
    government_nonprofit_charity = 'government_nonprofit.charity'
    healthcare = 'healthcare'
    healthcare_pharmaceutical = 'healthcare.pharmaceutical'
    healthcare_medical_devices = 'healthcare.medical_devices'
    healthcare_wellness = 'healthcare.wellness'
    home_garden = 'home_garden'
    home_garden_furniture = 'home_garden.furniture'
    home_garden_home_improvement = 'home_garden.home_improvement'
    media_entertainment = 'media_entertainment'
    media_entertainment_podcasts = 'media_entertainment.podcasts'
    media_entertainment_music = 'media_entertainment.music'
    media_entertainment_film_tv = 'media_entertainment.film_tv'
    media_entertainment_publishing = 'media_entertainment.publishing'
    media_entertainment_live_events = 'media_entertainment.live_events'
    pets = 'pets'
    professional_services = 'professional_services'
    professional_services_legal = 'professional_services.legal'
    professional_services_consulting = 'professional_services.consulting'
    real_estate = 'real_estate'
    real_estate_residential = 'real_estate.residential'
    real_estate_commercial = 'real_estate.commercial'
    recruitment_hr = 'recruitment_hr'
    retail = 'retail'
    retail_ecommerce = 'retail.ecommerce'
    retail_department_stores = 'retail.department_stores'
    sports_fitness = 'sports_fitness'
    sports_fitness_equipment = 'sports_fitness.equipment'
    sports_fitness_teams_leagues = 'sports_fitness.teams_leagues'
    technology = 'technology'
    technology_software = 'technology.software'
    technology_hardware = 'technology.hardware'
    technology_ai_ml = 'technology.ai_ml'
    telecom = 'telecom'
    telecom_mobile_carriers = 'telecom.mobile_carriers'
    telecom_internet_providers = 'telecom.internet_providers'
    transportation_logistics = 'transportation_logistics'
    travel_hospitality = 'travel_hospitality'
    travel_hospitality_airlines = 'travel_hospitality.airlines'
    travel_hospitality_hotels = 'travel_hospitality.hotels'
    travel_hospitality_cruise = 'travel_hospitality.cruise'
    travel_hospitality_tourism = 'travel_hospitality.tourism'

Enum where members are also (and must be) strings

Ancestors

  • enum.StrEnum
  • builtins.str
  • enum.ReprEnum
  • enum.Enum

Class variables

var automotive
var automotive_electric_vehicles
var automotive_luxury
var automotive_parts_accessories
var beauty_cosmetics
var beauty_cosmetics_fragrance
var beauty_cosmetics_haircare
var beauty_cosmetics_skincare
var cannabis
var cpg
var cpg_household
var cpg_personal_care
var dating
var education
var education_higher_education
var education_k12
var education_online_learning
var energy_utilities
var energy_utilities_renewable
var fashion_apparel
var fashion_apparel_luxury
var fashion_apparel_sportswear
var finance
var finance_banking
var finance_cryptocurrency
var finance_insurance
var finance_investment
var food_beverage
var food_beverage_alcohol
var food_beverage_packaged_goods
var food_beverage_restaurants
var gambling_betting
var gambling_betting_casino
var gambling_betting_sports_betting
var gaming
var gaming_console_pc
var gaming_esports
var gaming_mobile
var government_nonprofit
var government_nonprofit_charity
var government_nonprofit_political
var healthcare
var healthcare_medical_devices
var healthcare_pharmaceutical
var healthcare_wellness
var home_garden
var home_garden_furniture
var home_garden_home_improvement
var media_entertainment
var media_entertainment_film_tv
var media_entertainment_live_events
var media_entertainment_music
var media_entertainment_podcasts
var media_entertainment_publishing
var pets
var professional_services
var professional_services_consulting
var real_estate
var real_estate_commercial
var real_estate_residential
var recruitment_hr
var retail
var retail_department_stores
var retail_ecommerce
var sports_fitness
var sports_fitness_equipment
var sports_fitness_teams_leagues
var technology
var technology_ai_ml
var technology_hardware
var technology_software
var telecom
var telecom_internet_providers
var telecom_mobile_carriers
var transportation_logistics
var travel_hospitality
var travel_hospitality_airlines
var travel_hospitality_cruise
var travel_hospitality_hotels
var travel_hospitality_tourism
class AgentAuthorizationsDirectoryResult (**data: Any)
Expand source code
class AgentAuthorizationsDirectoryResult(AdCPBaseModel):
    """Response envelope for ``GET /v1/agents/{agent_url}/publishers``.

    Maps directly to ``schemas/aao/agent-publishers.json`` in the AdCP
    bundle (adcp#4828). The directory is a discovery accelerator — each
    ``publisher_domain`` row tells callers where to look; they SHOULD
    verify the publisher's adagents.json directly before treating an
    authorization as trusted.
    """

    agent_url: str
    directory_indexed_at: datetime | None
    publishers: list[DirectoryPublisherEntry] = Field(default_factory=list)
    next_cursor: str | None = None

Response envelope for GET /v1/agents/{agent_url}/publishers.

Maps directly to schemas/aao/agent-publishers.json in the AdCP bundle (adcp#4828). The directory is a discovery accelerator — each publisher_domain row tells callers where to look; they SHOULD verify the publisher's adagents.json directly before treating an authorization as trusted.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var agent_url : str
var directory_indexed_at : datetime.datetime | None
var model_config
var next_cursor : str | None
var publishers : list[DirectoryPublisherEntry]

Inherited members

class AgentCapabilities (**data: Any)
Expand source code
class AgentCapabilities(RegistryBaseModel):
    tools_count: int
    tools: list[AgentTool] | None = None
    standard_operations: AgentStandardOperations | None = None
    creative_capabilities: AgentCreativeCapabilities | None = None
    signals_capabilities: SignalsCapabilities | None = None
    measurement_capabilities: Annotated[
        MeasurementCapabilities | None,
        Field(
            description="Vendor-published per-metric catalog for measurement agents. Populated when the crawler successfully fetched and validated `get_adcp_capabilities.measurement` (AdCP 3.x). Mirrors the protocol shape — see the AdCP `get_adcp_capabilities` reference for field semantics."
        ),
    ] = None

Base model for registry API types.

Uses extra='allow' so that new fields from the registry API are preserved rather than dropped. This differs from AdCPBaseModel which defaults to extra='ignore' for protocol types.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var creative_capabilitiesAgentCreativeCapabilities | None
var measurement_capabilitiesMeasurementCapabilities | None
var model_config
var signals_capabilitiesSignalsCapabilities | None
var standard_operationsAgentStandardOperations | None
var tools : list[AgentTool] | None
var tools_count : int
class AgentCompliance (**data: Any)
Expand source code
class AgentCompliance(RegistryBaseModel):
    status: ComplianceStatus
    requested_compliance_target: Annotated[
        str | None,
        Field(
            description="Requested compliance target before alias resolution, e.g. 3.0 or 3.1-beta."
        ),
    ] = None
    adcp_version: Annotated[
        str | None,
        Field(
            description="Concrete AdCP compliance bundle version used for the latest run, e.g. 3.0.12."
        ),
    ] = None
    lifecycle_stage: AgentLifecycleStage
    tracks: Annotated[dict[str, str], Field(examples=[{"core": "pass", "products": "fail"}])]
    track_details: Annotated[
        list[TrackDetail] | None,
        Field(
            description="Latest-run per-track summary. Skipped tracks with has_coverage_gap_skip=true represent selected coverage gaps, such as missing_test_controller."
        ),
    ] = None
    streak_days: int
    last_checked_at: str | None
    headline: str | None
    monitoring_paused: bool | None = None
    check_interval_hours: int | None = None
    verified: bool | None = None
    verified_roles: Annotated[
        list[VerifiedRole] | None,
        Field(
            description="AdCP protocols the agent is AAO Verified for (e.g. media-buy, creative). Matches enums/adcp-protocol.json."
        ),
    ] = None

Base model for registry API types.

Uses extra='allow' so that new fields from the registry API are preserved rather than dropped. This differs from AdCPBaseModel which defaults to extra='ignore' for protocol types.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var adcp_version : str | None
var check_interval_hours : int | None
var headline : str | None
var last_checked_at : str | None
var lifecycle_stageAgentLifecycleStage
var model_config
var monitoring_paused : bool | None
var requested_compliance_target : str | None
var statusComplianceStatus
var streak_days : int
var track_details : list[TrackDetail] | None
var tracks : dict[str, str]
var verified : bool | None
var verified_roles : list[VerifiedRole] | None
class AgentConfig (**data: Any)
Expand source code
class AgentConfig(BaseModel):
    """Agent configuration."""

    id: str
    agent_uri: str
    protocol: Protocol
    auth_token: str | None = None
    requires_auth: bool = False
    auth_header: str = "x-adcp-auth"  # Header name for authentication
    auth_type: str = "token"  # "token" for direct value, "bearer" for "Bearer {token}"
    timeout: float = 30.0  # Request timeout in seconds
    mcp_transport: str = (
        "streamable_http"  # "streamable_http" (default, modern) or "sse" (legacy fallback)
    )
    debug: bool = False  # Enable debug mode to capture request/response details
    extra_headers: dict[str, str] = Field(default_factory=dict)
    """Additional HTTP headers sent on every request to this agent.

    This is a **transport-layer escape hatch**, not an AdCP protocol
    extension point — protocol-defined fields belong in the request
    envelope or ``RequestContext.metadata``. Use this for vendor or
    deployment-specific routing headers (e.g. tenant routing on a
    multi-tenant server).

    Reserved: the configured ``auth_header`` (default ``x-adcp-auth``)
    and the standard ``Authorization`` header — set credentials via
    ``auth_token``/``auth_header`` instead. Header names are rejected
    if they contain CR/LF or other control characters.

    Persisted plaintext at ``~/.adcp/config.json`` when saved via the
    CLI — do not store credentials here.
    """

    @field_validator("agent_uri")
    @classmethod
    def validate_agent_uri(cls, v: str) -> str:
        """Validate agent URI format."""
        if not v:
            raise ValueError("agent_uri cannot be empty")

        if not v.startswith(("http://", "https://")):
            raise ValueError(
                f"agent_uri must start with http:// or https://, got: {v}\n"
                "Example: https://agent.example.com"
            )

        parsed = urlparse(v)
        if parsed.username or parsed.password:
            raise ValueError(
                "agent_uri must not include credentials; use auth_token/auth_header instead"
            )

        return v

    @field_validator("timeout")
    @classmethod
    def validate_timeout(cls, v: float) -> float:
        """Validate timeout is reasonable."""
        if v <= 0:
            raise ValueError(f"timeout must be positive, got: {v}")

        if v > 300:  # 5 minutes
            raise ValueError(
                f"timeout is very large ({v}s). Consider a value under 300 seconds.\n"
                "Large timeouts can cause long hangs if agent is unresponsive."
            )

        return v

    @field_validator("mcp_transport")
    @classmethod
    def validate_mcp_transport(cls, v: str) -> str:
        """Validate MCP transport type."""
        valid_transports = ["streamable_http", "sse"]
        if v not in valid_transports:
            raise ValueError(
                f"mcp_transport must be one of {valid_transports}, got: {v}\n"
                "Use 'streamable_http' for modern agents (recommended)"
            )
        return v

    @field_validator("auth_type")
    @classmethod
    def validate_auth_type(cls, v: str) -> str:
        """Validate auth type."""
        valid_types = ["token", "bearer"]
        if v not in valid_types:
            raise ValueError(
                f"auth_type must be one of {valid_types}, got: {v}\n"
                "Use 'bearer' for OAuth2/standard Authorization header"
            )
        return v

    @model_validator(mode="after")
    def _validate_security_constraints(self) -> AgentConfig:
        non_loopback_http = self.agent_uri.startswith("http://") and not is_loopback_http_uri(
            self.agent_uri
        )
        if self.auth_token and non_loopback_http:
            raise ValueError(
                "auth_token requires an https:// agent_uri for non-loopback hosts; "
                "plain HTTP is only allowed for localhost/loopback development"
            )

        if self.extra_headers and non_loopback_http:
            raise ValueError(
                "extra_headers require an https:// agent_uri for non-loopback hosts; "
                "plain HTTP is only allowed for localhost/loopback development"
            )

        if not self.extra_headers:
            return self
        reserved = {self.auth_header.lower(), "authorization"}
        for key, value in self.extra_headers.items():
            if not key:
                raise ValueError("extra_headers contains an empty header name")
            if any(c in key for c in ("\r", "\n", "\x00")) or any(ord(c) < 0x20 for c in key):
                raise ValueError(f"extra_headers key contains control character: {key!r}")
            if any(c in value for c in ("\r", "\n", "\x00")):
                raise ValueError(f"extra_headers value for {key!r} contains CR/LF/NUL")
            if key.lower() in reserved:
                raise ValueError(
                    f"extra_headers may not override reserved auth header "
                    f"{key!r} (collides with auth_header={self.auth_header!r} "
                    f"or 'Authorization'); set credentials via auth_token + "
                    f"auth_header instead"
                )
        return self

Agent configuration.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • pydantic.main.BaseModel

Class variables

var agent_uri : str
var auth_header : str
var auth_token : str | None
var auth_type : str
var debug : bool
var extra_headers : dict[str, str]

Additional HTTP headers sent on every request to this agent.

This is a transport-layer escape hatch, not an AdCP protocol extension point — protocol-defined fields belong in the request envelope or RequestContext.metadata. Use this for vendor or deployment-specific routing headers (e.g. tenant routing on a multi-tenant server).

Reserved: the configured auth_header (default x-adcp-auth) and the standard Authorization header — set credentials via auth_token/auth_header instead. Header names are rejected if they contain CR/LF or other control characters.

Persisted plaintext at ~/.adcp/config.json when saved via the CLI — do not store credentials here.

var id : str
var mcp_transport : str
var model_config
var protocolProtocol
var requires_auth : bool
var timeout : float

Static methods

def validate_agent_uri(v: str) ‑> str

Validate agent URI format.

def validate_auth_type(v: str) ‑> str

Validate auth type.

def validate_mcp_transport(v: str) ‑> str

Validate MCP transport type.

def validate_timeout(v: float) ‑> float

Validate timeout is reasonable.

class AgentHealth (**data: Any)
Expand source code
class AgentHealth(RegistryBaseModel):
    online: bool
    checked_at: str
    response_time_ms: float | None = None
    tools_count: int | None = None
    resources_count: int | None = None
    error: str | None = None

Base model for registry API types.

Uses extra='allow' so that new fields from the registry API are preserved rather than dropped. This differs from AdCPBaseModel which defaults to extra='ignore' for protocol types.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var checked_at : str
var error : str | None
var model_config
var online : bool
var resources_count : int | None
var response_time_ms : float | None
var tools_count : int | None
class AgentStats (**data: Any)
Expand source code
class AgentStats(RegistryBaseModel):
    property_count: int | None = None
    publisher_count: int | None = None
    publishers: list[str] | None = None
    creative_formats: int | None = None

Base model for registry API types.

Uses extra='allow' so that new fields from the registry API are preserved rather than dropped. This differs from AdCPBaseModel which defaults to extra='ignore' for protocol types.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var creative_formats : int | None
var model_config
var property_count : int | None
var publisher_count : int | None
var publishers : list[str] | None
class ArtifactWebhookPayload (**data: Any)
Expand source code
class ArtifactWebhookPayload(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    idempotency_key: Annotated[
        str,
        Field(
            description='Sender-generated key stable across retries of the same webhook event. Sales agents MUST generate a cryptographically random value (UUID v4 recommended) per distinct emission of a batch and reuse the same key on every retry. Recipients MUST dedupe by this key, scoped to the authenticated sender identity (HMAC secret or Bearer credential) — keys from different sales agents are independent. Distinct from `batch_id`, which identifies the logical batch: `idempotency_key` identifies this specific emission event, so a re-emission of the same `batch_id` (e.g., after a correction) is a different event and MUST carry a fresh `idempotency_key`.',
            max_length=255,
            min_length=16,
            pattern='^[A-Za-z0-9_.:-]{16,255}$',
        ),
    ]
    media_buy_id: Annotated[
        str, Field(description='Media buy identifier these artifacts belong to')
    ]
    batch_id: Annotated[
        str,
        Field(
            description='Unique identifier for this batch of artifacts. Use for deduplication and acknowledgment.'
        ),
    ]
    timestamp: Annotated[
        AwareDatetime, Field(description='When this batch was generated (ISO 8601)')
    ]
    artifacts: Annotated[
        list[Artifact], Field(description='Content artifacts from delivered impressions')
    ]
    pagination: Annotated[
        Pagination | None, Field(description='Pagination info when batching large artifact sets')
    ] = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var artifacts : list[adcp.types.generated_poc.content_standards.artifact_webhook_payload.Artifact]
var batch_id : str
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var idempotency_key : str
var media_buy_id : str
var model_config
var pagination : adcp.types.generated_poc.content_standards.artifact_webhook_payload.Pagination | None
var timestamp : pydantic.types.AwareDatetime

Inherited members

class AssetContentType (*args, **kwds)
Expand source code
class AssetContentType(StrEnum):
    image = 'image'
    video = 'video'
    audio = 'audio'
    text = 'text'
    markdown = 'markdown'
    html = 'html'
    css = 'css'
    javascript = 'javascript'
    zip = 'zip'
    vast = 'vast'
    daast = 'daast'
    url = 'url'
    webhook = 'webhook'
    brief = 'brief'
    catalog = 'catalog'
    published_post = 'published_post'

Enum where members are also (and must be) strings

Ancestors

  • enum.StrEnum
  • builtins.str
  • enum.ReprEnum
  • enum.Enum

Class variables

var audio
var brief
var catalog
var css
var daast
var html
var image
var javascript
var markdown
var published_post
var text
var url
var vast
var video
var webhook
var zip
class AssetVariant (root: RootModelRootType = PydanticUndefined, **data)
Expand source code
class AssetVariant(
    RootModel[
        ImageAsset
        | VideoAsset
        | AudioAsset
        | VastAsset
        | TextAsset
        | UrlAsset
        | HtmlAsset
        | JavascriptAsset
        | ZipAsset
        | WebhookAsset
        | CssAsset
        | DaastAsset
        | MarkdownAsset
        | BriefAsset
        | CatalogAsset
        | PublishedPostAsset
        | CardAsset
        | PixelTrackerAsset
        | VastTrackerAsset
        | DaastTrackerAsset
    ]
):
    root: Annotated[
        ImageAsset
        | VideoAsset
        | AudioAsset
        | VastAsset
        | TextAsset
        | UrlAsset
        | HtmlAsset
        | JavascriptAsset
        | ZipAsset
        | WebhookAsset
        | CssAsset
        | DaastAsset
        | MarkdownAsset
        | BriefAsset
        | CatalogAsset
        | PublishedPostAsset
        | CardAsset
        | PixelTrackerAsset
        | VastTrackerAsset
        | DaastTrackerAsset,
        Field(
            description='Canonical union of all asset variant schemas. Referenced from creative-asset.json and creative-manifest.json to ensure a single named type is emitted by schema-to-TypeScript tooling. Add new asset types here and to the creative/asset-types registry.',
            discriminator='asset_type',
            title='AssetVariant',
        ),
    ]
    def __getattr__(self, name: str) -> Any:
        """Proxy attribute access to the wrapped type."""
        if name.startswith('_'):
            raise AttributeError(name)
        return getattr(self.root, name)

Usage Documentation

RootModel and Custom Root Types

A Pydantic BaseModel for the root object of the model.

Attributes
-----=
root
The root object of the model.
__pydantic_root_model__
Whether the model is a RootModel.
__pydantic_private__
Private fields in the model.
__pydantic_extra__
Extra fields in the model.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • pydantic.root_model.RootModel[Union[ImageAsset, VideoAsset, AudioAsset, VastAsset, TextAsset, UrlAsset, HtmlAsset, JavascriptAsset, ZipAsset, WebhookAsset, CssAsset, DaastAsset, MarkdownAsset, BriefAsset, CatalogAsset, PublishedPostAsset, CardAsset, PixelTrackerAsset, VastTrackerAsset, DaastTrackerAsset]]
  • pydantic.root_model.RootModel
  • pydantic.main.BaseModel
  • typing.Generic

Class variables

var model_config
var root : adcp.types.generated_poc.core.assets.asset_union.ImageAsset | adcp.types.generated_poc.core.assets.asset_union.VideoAsset | adcp.types.generated_poc.core.assets.asset_union.AudioAsset | adcp.types.generated_poc.core.assets.asset_union.VastAsset | adcp.types.generated_poc.core.assets.asset_union.TextAsset | adcp.types.generated_poc.core.assets.asset_union.UrlAsset | adcp.types.generated_poc.core.assets.asset_union.HtmlAsset | adcp.types.generated_poc.core.assets.asset_union.JavascriptAsset | adcp.types.generated_poc.core.assets.asset_union.ZipAsset | adcp.types.generated_poc.core.assets.asset_union.WebhookAsset | adcp.types.generated_poc.core.assets.asset_union.CssAsset | adcp.types.generated_poc.core.assets.asset_union.DaastAsset | adcp.types.generated_poc.core.assets.asset_union.MarkdownAsset | adcp.types.generated_poc.core.assets.asset_union.BriefAsset | adcp.types.generated_poc.core.assets.asset_union.CatalogAsset | adcp.types.generated_poc.core.assets.asset_union.PublishedPostAsset | adcp.types.generated_poc.core.assets.asset_union.CardAsset | adcp.types.generated_poc.core.assets.asset_union.PixelTrackerAsset | adcp.types.generated_poc.core.assets.asset_union.VastTrackerAsset | adcp.types.generated_poc.core.assets.asset_union.DaastTrackerAsset
class SyncAudiencesAudience (**data: Any)
Expand source code
class Audience(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    audience_id: Annotated[
        str,
        Field(
            description="Buyer's identifier for this audience. Used to reference the audience in targeting overlays."
        ),
    ]
    name: Annotated[str | None, Field(description='Human-readable name for this audience')] = None
    description: Annotated[
        str | None,
        Field(
            description="Human-readable description of this audience's composition or purpose (e.g., 'High-value customers who purchased in the last 90 days')."
        ),
    ] = None
    audience_type: Annotated[
        AudienceType | None,
        Field(
            description="Intended use for this audience. 'crm': target these users. 'suppression': exclude these users from delivery. 'lookalike_seed': use as a seed for the seller's lookalike modeling. Sellers may handle audiences differently based on type (e.g., suppression lists bypass minimum size requirements on some platforms)."
        ),
    ] = None
    tags: Annotated[
        list[Tag] | None,
        Field(
            description="Buyer-defined tags for organizing and filtering audiences (e.g., 'holiday_2026', 'high_ltv'). Tags are stored by the seller and returned in discovery-only calls."
        ),
    ] = None
    add: Annotated[
        list[audience_member.AudienceMember] | None,
        Field(
            description='Members to add to this audience. Hashed before sending — normalize emails to lowercase+trim, phones to E.164.',
            min_length=1,
        ),
    ] = None
    remove: Annotated[
        list[audience_member.AudienceMember] | None,
        Field(
            description='Members to remove from this audience. If the same identifier appears in both add and remove in a single request, remove takes precedence.',
            min_length=1,
        ),
    ] = None
    delete: Annotated[
        bool | None,
        Field(
            description='When true, delete this audience from the account entirely. All other fields on this audience object are ignored. Use this to delete a specific audience without affecting others.'
        ),
    ] = None
    consent_basis: Annotated[
        consent_basis_1.ConsentBasis | None,
        Field(
            description='GDPR lawful basis for processing this audience list. Informational — not validated by the protocol, but required by some sellers operating in regulated markets (e.g. EU). When omitted, the buyer asserts they have a lawful basis appropriate to their jurisdiction.'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var add : list[adcp.types.generated_poc.core.audience_member.AudienceMember] | None
var audience_id : str
var audience_type : adcp.types.generated_poc.media_buy.sync_audiences_request.AudienceType | None
var consent_basis : adcp.types.generated_poc.enums.consent_basis.ConsentBasis | None
var delete : bool | None
var description : str | None
var model_config
var name : str | None
var remove : list[adcp.types.generated_poc.core.audience_member.AudienceMember] | None
var tags : list[adcp.types.generated_poc.media_buy.sync_audiences_request.Tag] | None

Inherited members

class AudienceSource (*args, **kwds)
Expand source code
class AudienceSource(StrEnum):
    synced = 'synced'
    platform = 'platform'
    third_party = 'third_party'
    lookalike = 'lookalike'
    retargeting = 'retargeting'
    unknown = 'unknown'

Enum where members are also (and must be) strings

Ancestors

  • enum.StrEnum
  • builtins.str
  • enum.ReprEnum
  • enum.Enum

Class variables

var lookalike
var platform
var retargeting
var synced
var third_party
var unknown
class AudioContent (**data: Any)
Expand source code
class AudioAsset(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    asset_type: Annotated[
        Literal['audio'],
        Field(
            description='Discriminator identifying this as an audio asset. See /schemas/creative/asset-types for the registry.'
        ),
    ] = 'audio'
    url: Annotated[AnyUrl, Field(description='URL to the audio asset')]
    duration_ms: Annotated[
        int | None, Field(description='Audio duration in milliseconds', ge=0)
    ] = None
    file_size_bytes: Annotated[int | None, Field(description='File size in bytes', ge=1)] = None
    container_format: Annotated[
        str | None,
        Field(description='Audio container/file format (mp3, m4a, aac, wav, ogg, flac, etc.)'),
    ] = None
    codec: Annotated[
        str | None,
        Field(
            description='Audio codec used (aac, aac_lc, he_aac, pcm, mp3, vorbis, opus, flac, ac3, eac3, etc.)'
        ),
    ] = None
    sampling_rate_hz: Annotated[
        int | None, Field(description='Sampling rate in Hz (e.g., 44100, 48000, 96000)')
    ] = None
    channels: Annotated[
        audio_channel_layout.AudioChannelLayout | None, Field(description='Channel configuration')
    ] = None
    bit_depth: Annotated[BitDepth | None, Field(description='Bit depth')] = None
    bitrate_kbps: Annotated[
        int | None, Field(description='Bitrate in kilobits per second', ge=1)
    ] = None
    loudness_lufs: Annotated[float | None, Field(description='Integrated loudness in LUFS')] = None
    true_peak_dbfs: Annotated[float | None, Field(description='True peak level in dBFS')] = None
    transcript_url: Annotated[
        AnyUrl | None, Field(description='URL to text transcript of the audio content')
    ] = None
    provenance: Annotated[
        provenance_1.Provenance | None,
        Field(
            description='Provenance metadata for this asset, overrides manifest-level provenance'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var asset_type : Literal['audio']
var bit_depth : adcp.types.generated_poc.core.assets.audio_asset.BitDepth | None
var bitrate_kbps : int | None
var channels : adcp.types.generated_poc.enums.audio_channel_layout.AudioChannelLayout | None
var codec : str | None
var container_format : str | None
var duration_ms : int | None
var file_size_bytes : int | None
var loudness_lufs : float | None
var model_config
var provenance : adcp.types.generated_poc.core.provenance.Provenance | None
var sampling_rate_hz : int | None
var transcript_url : pydantic.networks.AnyUrl | None
var true_peak_dbfs : float | None
var url : pydantic.networks.AnyUrl

Inherited members

class AuthorizationContext (properties: list[Any])
Expand source code
class AuthorizationContext:
    """Authorization context for a publisher domain.

    Attributes:
        property_ids: List of property IDs the agent is authorized for
        property_tags: List of property tags the agent is authorized for
        raw_properties: Raw property data from adagents.json
    """

    def __init__(self, properties: list[Any]):
        """Initialize from list of properties.

        Args:
            properties: List of property dictionaries from adagents.json
        """
        self.property_ids: list[str] = []
        self.property_tags: list[str] = []
        self.raw_properties = properties

        # Extract property IDs and tags
        for prop in properties:
            if not isinstance(prop, dict):
                continue

            # Extract property ID (per AdCP v2 schema, the field is "property_id")
            prop_id = prop.get("property_id")
            if prop_id and isinstance(prop_id, str):
                self.property_ids.append(prop_id)

            # Extract tags
            tags = prop.get("tags", [])
            if isinstance(tags, list):
                for tag in tags:
                    if isinstance(tag, str) and tag not in self.property_tags:
                        self.property_tags.append(tag)

    def __repr__(self) -> str:
        return (
            f"AuthorizationContext("
            f"property_ids={self.property_ids}, "
            f"property_tags={self.property_tags})"
        )

Authorization context for a publisher domain.

Attributes
-----=
property_ids
List of property IDs the agent is authorized for
property_tags
List of property tags the agent is authorized for
raw_properties
Raw property data from adagents.json

Initialize from list of properties.

Args
-----=
properties
List of property dictionaries from adagents.json
class AuthorizationRequiredDetails (**data: Any)
Expand source code
class AuthorizationRequiredDetails(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    required_connections: Annotated[
        list[downstream_connection_requirement.DownstreamConnectionRequirement] | None,
        Field(
            description='Complete set of downstream connections known to be required for the relevant product, format, or request.'
        ),
    ] = None
    missing_connections: Annotated[
        list[downstream_connection_requirement.DownstreamConnectionRequirement] | None,
        Field(
            description='Subset of downstream connections that blocked the current request. Sellers SHOULD populate this array when the caller needs to route a human through a connections flow. Entries with `status` of `missing`, `pending`, `expired`, or `revoked` MUST include either `provider` or `authorization_url` so the buyer can route the remediation unambiguously.'
        ),
    ] = None
    authorization_url: Annotated[
        AnyUrl | None,
        Field(
            description='General recovery URL when there is a single obvious authorization step or when the seller has its own connection-management page.'
        ),
    ] = None
    authorization_instructions: Annotated[
        str | None,
        Field(
            description='Human-readable recovery instructions. Use `missing_connections[].authorization_instructions` when instructions differ per downstream connection.'
        ),
    ] = None
    reference_authorization: Annotated[
        dict[str, Any] | None,
        Field(
            deprecated=True,
            description='Legacy or provider-specific authorization hint for the referenced object. Prefer `missing_connections[]` for new implementations.',
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var authorization_instructions : str | None
var authorization_url : pydantic.networks.AnyUrl | None
var missing_connections : list[adcp.types.generated_poc.core.downstream_connection_requirement.DownstreamConnectionRequirement] | None
var model_config
var reference_authorization : dict[str, typing.Any] | None
var required_connections : list[adcp.types.generated_poc.core.downstream_connection_requirement.DownstreamConnectionRequirement] | None

Inherited members

class AuthorizedAgentsByPropertyId (**data: Any)
Expand source code
class AuthorizedAgents1(AuthorizedAgentBaseFields):
    model_config = ConfigDict(
        extra='allow',
    )
    authorization_type: Annotated[
        Literal['property_ids'],
        Field(description='Discriminator indicating authorization by specific property IDs'),
    ] = 'property_ids'
    property_ids: Annotated[
        list[property_id.PropertyId],
        Field(
            description='Property IDs this agent is authorized for. Resolved against the top-level properties array in this file',
            min_length=1,
        ),
    ]
    collections: Annotated[
        list[collection_selector.CollectionSelector] | None,
        Field(
            description='Optional collection constraints. When present, authorization only applies to inventory associated with these collections.',
            min_length=1,
        ),
    ] = None
    placement_ids: Annotated[
        list[str] | None,
        Field(
            description='Optional placement constraints. When present, authorization only applies to these placement IDs from the top-level placements array in this file.',
            min_length=1,
        ),
    ] = None
    placement_tags: Annotated[
        list[str] | None,
        Field(
            description='Optional placement tag constraints. When present, authorization only applies to placements whose tags include any of these publisher-defined values.',
            min_length=1,
        ),
    ] = None
    delegation_type: Annotated[
        DelegationType | None,
        Field(
            description="Commercial relationship for this inventory path. 'direct' means the publisher treats this as a direct way to buy from them, even if a third party operates the software. 'delegated' means the agent is authorized to sell on the publisher's behalf. 'ad_network' means the inventory is sold as part of a network/package context rather than as the publisher's direct endpoint."
        ),
    ] = None
    exclusive: Annotated[
        bool | None,
        Field(
            description="Whether this agent is the publisher's sole authorized path for the scoped inventory slice. When false or absent, other authorized agents may also sell the same inventory."
        ),
    ] = None
    countries: Annotated[
        list[Country] | None,
        Field(
            description='Optional ISO 3166-1 alpha-2 country codes limiting where this authorization applies. Omit for worldwide authorization.',
            min_length=1,
        ),
    ] = None
    effective_from: Annotated[
        AwareDatetime | None,
        Field(description='Optional start time for this authorization window.'),
    ] = None
    effective_until: Annotated[
        AwareDatetime | None, Field(description='Optional end time for this authorization window.')
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.authorized_agent_base.AuthorizedAgentBaseFields
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var authorization_type : Literal['property_ids']
var collections : list[adcp.types.generated_poc.core.collection_selector.CollectionSelector] | None
var countries : list[adcp.types.generated_poc.adagents.Country] | None
var delegation_type : adcp.types.generated_poc.adagents.DelegationType | None
var effective_from : pydantic.types.AwareDatetime | None
var effective_until : pydantic.types.AwareDatetime | None
var exclusive : bool | None
var model_config
var placement_ids : list[str] | None
var placement_tags : list[str] | None
var property_ids : list[adcp.types.generated_poc.core.property_id.PropertyId]

Inherited members

class AuthorizedAgentsByPropertyTag (**data: Any)
Expand source code
class AuthorizedAgents2(AuthorizedAgentBaseFields):
    model_config = ConfigDict(
        extra='allow',
    )
    authorization_type: Annotated[
        Literal['property_tags'],
        Field(description='Discriminator indicating authorization by property tags'),
    ] = 'property_tags'
    property_tags: Annotated[
        list[property_tag.PropertyTag],
        Field(
            description='Tags identifying which properties this agent is authorized for. Resolved against the top-level properties array in this file using tag matching',
            min_length=1,
        ),
    ]
    collections: Annotated[
        list[collection_selector.CollectionSelector] | None,
        Field(
            description='Optional collection constraints. When present, authorization only applies to inventory associated with these collections.',
            min_length=1,
        ),
    ] = None
    placement_ids: Annotated[
        list[str] | None,
        Field(
            description='Optional placement constraints. When present, authorization only applies to these placement IDs from the top-level placements array in this file.',
            min_length=1,
        ),
    ] = None
    placement_tags: Annotated[
        list[str] | None,
        Field(
            description='Optional placement tag constraints. When present, authorization only applies to placements whose tags include any of these publisher-defined values.',
            min_length=1,
        ),
    ] = None
    delegation_type: Annotated[
        DelegationType | None,
        Field(
            description="Commercial relationship for this inventory path. 'direct' means the publisher treats this as a direct way to buy from them, even if a third party operates the software. 'delegated' means the agent is authorized to sell on the publisher's behalf. 'ad_network' means the inventory is sold as part of a network/package context rather than as the publisher's direct endpoint."
        ),
    ] = None
    exclusive: Annotated[
        bool | None,
        Field(
            description="Whether this agent is the publisher's sole authorized path for the scoped inventory slice. When false or absent, other authorized agents may also sell the same inventory."
        ),
    ] = None
    countries: Annotated[
        list[Country] | None,
        Field(
            description='Optional ISO 3166-1 alpha-2 country codes limiting where this authorization applies. Omit for worldwide authorization.',
            min_length=1,
        ),
    ] = None
    effective_from: Annotated[
        AwareDatetime | None,
        Field(description='Optional start time for this authorization window.'),
    ] = None
    effective_until: Annotated[
        AwareDatetime | None, Field(description='Optional end time for this authorization window.')
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.authorized_agent_base.AuthorizedAgentBaseFields
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var authorization_type : Literal['property_tags']
var collections : list[adcp.types.generated_poc.core.collection_selector.CollectionSelector] | None
var countries : list[adcp.types.generated_poc.adagents.Country] | None
var delegation_type : adcp.types.generated_poc.adagents.DelegationType | None
var effective_from : pydantic.types.AwareDatetime | None
var effective_until : pydantic.types.AwareDatetime | None
var exclusive : bool | None
var model_config
var placement_ids : list[str] | None
var placement_tags : list[str] | None
var property_tags : list[adcp.types.generated_poc.core.property_tag.PropertyTag]

Inherited members

class AuthorizedAgentsByInlineProperties (**data: Any)
Expand source code
class AuthorizedAgents3(AuthorizedAgentBaseFields):
    model_config = ConfigDict(
        extra='allow',
    )
    authorization_type: Annotated[
        Literal['inline_properties'],
        Field(
            description='Discriminator indicating authorization by inline property definitions. Companion field is `properties` (not `inline_properties`) — the only authorization_type whose companion field name does not mirror the discriminator value.'
        ),
    ] = 'inline_properties'
    properties: Annotated[
        list[property.Property],
        Field(
            description='Specific properties this agent is authorized for, defined inline on the agent entry (alternative to property_ids/property_tags). Note: this is the companion field for `authorization_type: "inline_properties"` — the field is named `properties`, not `inline_properties`.',
            min_length=1,
        ),
    ]
    collections: Annotated[
        list[collection_selector.CollectionSelector] | None,
        Field(
            description='Optional collection constraints. When present, authorization only applies to inventory associated with these collections.',
            min_length=1,
        ),
    ] = None
    placement_ids: Annotated[
        list[str] | None,
        Field(
            description='Optional placement constraints. When present, authorization only applies to these placement IDs from the top-level placements array in this file.',
            min_length=1,
        ),
    ] = None
    placement_tags: Annotated[
        list[str] | None,
        Field(
            description='Optional placement tag constraints. When present, authorization only applies to placements whose tags include any of these publisher-defined values.',
            min_length=1,
        ),
    ] = None
    delegation_type: Annotated[
        DelegationType | None,
        Field(
            description="Commercial relationship for this inventory path. 'direct' means the publisher treats this as a direct way to buy from them, even if a third party operates the software. 'delegated' means the agent is authorized to sell on the publisher's behalf. 'ad_network' means the inventory is sold as part of a network/package context rather than as the publisher's direct endpoint."
        ),
    ] = None
    exclusive: Annotated[
        bool | None,
        Field(
            description="Whether this agent is the publisher's sole authorized path for the scoped inventory slice. When false or absent, other authorized agents may also sell the same inventory."
        ),
    ] = None
    countries: Annotated[
        list[Country] | None,
        Field(
            description='Optional ISO 3166-1 alpha-2 country codes limiting where this authorization applies. Omit for worldwide authorization.',
            min_length=1,
        ),
    ] = None
    effective_from: Annotated[
        AwareDatetime | None,
        Field(description='Optional start time for this authorization window.'),
    ] = None
    effective_until: Annotated[
        AwareDatetime | None, Field(description='Optional end time for this authorization window.')
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.authorized_agent_base.AuthorizedAgentBaseFields
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var authorization_type : Literal['inline_properties']
var collections : list[adcp.types.generated_poc.core.collection_selector.CollectionSelector] | None
var countries : list[adcp.types.generated_poc.adagents.Country] | None
var delegation_type : adcp.types.generated_poc.adagents.DelegationType | None
var effective_from : pydantic.types.AwareDatetime | None
var effective_until : pydantic.types.AwareDatetime | None
var exclusive : bool | None
var model_config
var placement_ids : list[str] | None
var placement_tags : list[str] | None
var properties : list[adcp.types.generated_poc.core.property.Property]

Inherited members

class AuthorizedAgentsByPublisherProperties (**data: Any)
Expand source code
class AuthorizedAgents4(AuthorizedAgentBaseFields):
    model_config = ConfigDict(
        extra='allow',
    )
    authorization_type: Annotated[
        Literal['publisher_properties'],
        Field(
            description='Discriminator indicating authorization for properties from other publisher domains'
        ),
    ] = 'publisher_properties'
    publisher_properties: Annotated[
        list[publisher_property_selector.PublisherPropertySelector],
        Field(
            description='Properties from other publisher domains this agent is authorized for. Each entry specifies a publisher domain and which of their properties this agent can sell',
            min_length=1,
        ),
    ]
    collections: Annotated[
        list[collection_selector.CollectionSelector] | None,
        Field(
            description='Optional collection constraints. When present, authorization only applies to inventory associated with these collections.',
            min_length=1,
        ),
    ] = None
    placement_ids: Annotated[
        list[str] | None,
        Field(
            description='Optional placement constraints. When present, authorization only applies to these placement IDs from the top-level placements array in this file.',
            min_length=1,
        ),
    ] = None
    placement_tags: Annotated[
        list[str] | None,
        Field(
            description='Optional placement tag constraints. When present, authorization only applies to placements whose tags include any of these publisher-defined values.',
            min_length=1,
        ),
    ] = None
    delegation_type: Annotated[
        DelegationType | None,
        Field(
            description="Commercial relationship for this inventory path. 'direct' means the publisher treats this as a direct way to buy from them, even if a third party operates the software. 'delegated' means the agent is authorized to sell on the publisher's behalf. 'ad_network' means the inventory is sold as part of a network/package context rather than as the publisher's direct endpoint."
        ),
    ] = None
    exclusive: Annotated[
        bool | None,
        Field(
            description="Whether this agent is the publisher's sole authorized path for the scoped inventory slice. When false or absent, other authorized agents may also sell the same inventory."
        ),
    ] = None
    countries: Annotated[
        list[Country] | None,
        Field(
            description='Optional ISO 3166-1 alpha-2 country codes limiting where this authorization applies. Omit for worldwide authorization.',
            min_length=1,
        ),
    ] = None
    effective_from: Annotated[
        AwareDatetime | None,
        Field(description='Optional start time for this authorization window.'),
    ] = None
    effective_until: Annotated[
        AwareDatetime | None, Field(description='Optional end time for this authorization window.')
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.authorized_agent_base.AuthorizedAgentBaseFields
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var authorization_type : Literal['publisher_properties']
var collections : list[adcp.types.generated_poc.core.collection_selector.CollectionSelector] | None
var countries : list[adcp.types.generated_poc.adagents.Country] | None
var delegation_type : adcp.types.generated_poc.adagents.DelegationType | None
var effective_from : pydantic.types.AwareDatetime | None
var effective_until : pydantic.types.AwareDatetime | None
var exclusive : bool | None
var model_config
var placement_ids : list[str] | None
var placement_tags : list[str] | None
var publisher_properties : list[adcp.types.generated_poc.core.publisher_property_selector.PublisherPropertySelector]

Inherited members

class AuthorizedAgentsBySignalId (**data: Any)
Expand source code
class AuthorizedAgents5(AuthorizedAgentBaseFields):
    model_config = ConfigDict(
        extra='allow',
    )
    authorization_type: Annotated[
        Literal['signal_ids'],
        Field(description='Discriminator indicating authorization by specific signal IDs'),
    ] = 'signal_ids'
    signal_ids: Annotated[
        list[SignalId],
        Field(
            description='Signal IDs this agent is authorized to resell. Resolved against the top-level signals array in this file',
            min_length=1,
        ),
    ]

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.authorized_agent_base.AuthorizedAgentBaseFields
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var authorization_type : Literal['signal_ids']
var model_config
var signal_ids : list[adcp.types.generated_poc.adagents.SignalId]

Inherited members

class AuthorizedAgentsBySignalTag (**data: Any)
Expand source code
class AuthorizedAgents6(AuthorizedAgentBaseFields):
    model_config = ConfigDict(
        extra='allow',
    )
    authorization_type: Annotated[
        Literal['signal_tags'],
        Field(description='Discriminator indicating authorization by signal tags'),
    ] = 'signal_tags'
    signal_tags: Annotated[
        list[SignalTag],
        Field(
            description='Signal tags this agent is authorized for. Agent can resell all signals with these tags',
            min_length=1,
        ),
    ]

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.authorized_agent_base.AuthorizedAgentBaseFields
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var authorization_type : Literal['signal_tags']
var model_config
var signal_tags : list[adcp.types.generated_poc.adagents.SignalTag]

Inherited members

class BrandIdentity (**data: Any)
Expand source code
class Brand(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    id: Annotated[
        BrandId, Field(description='Brand identifier within the house. House chooses this ID.')
    ]
    url: Annotated[
        AnyUrl | None, Field(description='Primary brand URL for context and asset discovery')
    ] = None
    identity_relying_parties: Annotated[
        list[IdentityRelyingParty] | None,
        Field(
            description='Verified-identity relying parties scoped to this brand/property, for attestation provenance in TMP Identity Match. Use when a brand or property runs its own relying_party_id (per-property pseudonyms); entity-wide relying parties live on the house object. See specs/tmp-verified-identity-attestation.md.'
        ),
    ] = None
    names: Annotated[
        list[LocalizedName],
        Field(
            description='Localized brand names. Multiple entries per language allowed for aliases.',
            min_length=1,
        ),
    ]
    keller_type: KellerType | None = None
    parent_brand: Annotated[
        BrandId | None, Field(description='Parent brand ID for sub-brands and endorsed brands')
    ] = None
    description: Annotated[str | None, Field(description='Brand description')] = None
    industries: Annotated[
        list[str] | None,
        Field(
            description="Brand industries (e.g., ['automotive'] or ['pharmaceutical', 'cpg'] for a consumer health company). Describes what the company does — not what regulatory regimes apply (use policy_categories for that).",
            min_length=1,
        ),
    ] = None
    target_audience: Annotated[str | None, Field(description='Primary target audience')] = None
    logos: Annotated[list[Logo] | None, Field(description='Brand logo assets')] = None
    colors: Colors | None = None
    fonts: Fonts | None = None
    tone: Annotated[
        str | Tone | None, Field(description='Brand voice and messaging tone guidelines')
    ] = None
    tagline: Annotated[
        str | Tagline | None,
        Field(
            description='Brand tagline or slogan. Accepts a plain string or a localized array matching the names pattern.'
        ),
    ] = None
    assets: Annotated[list[Asset] | None, Field(description='Brand asset library')] = None
    properties: Annotated[
        list[Property] | None,
        Field(
            description='Digital properties associated with this brand — owned, managed, or represented'
        ),
    ] = None
    product_catalog: ProductCatalog | None = None
    privacy_policy_url: Annotated[
        AnyUrl | None, Field(description="URL to the brand's privacy policy")
    ] = None
    data_subject_contestation: DataSubjectContestation | None = None
    disclaimers: Annotated[
        list[Disclaimer] | None, Field(description='Legal disclaimers for creatives')
    ] = None
    trademarks: Annotated[
        list[Trademark] | None,
        Field(
            description="Brand-level registered trademarks. Use for marks the brand owns or controls (e.g., a sub-brand's own marks distinct from the corporate parent). House-level trademarks live on the house object; resolution between the two is union — both lists are valid claims."
        ),
    ] = None
    voice_synthesis: Annotated[
        VoiceSynthesis | None,
        Field(description='TTS voice synthesis configuration for AI-generated audio'),
    ] = None
    avatar: Annotated[Avatar | None, Field(description='Visual avatar configuration')] = None
    visual_guidelines: Annotated[
        VisualGuidelines | None,
        Field(description='Structured visual rules for generative creative systems'),
    ] = None
    agents: Annotated[
        Agents | None,
        Field(
            description='Agents authorized to act on behalf of this brand. Consumers resolving an agent by URL use the matching brand-level entry; do not infer a type-wide override of unrelated house-level entries when multiple same-type entries exist.'
        ),
    ] = None
    brand_agent: Annotated[
        BrandAgent | None,
        Field(
            deprecated=True,
            description="Deprecated: use agents array with type 'brand' instead. Brand agent that provides dynamic brand data via MCP.",
        ),
    ] = None
    rights_agent: Annotated[
        RightsAgent | None,
        Field(
            deprecated=True,
            description="Deprecated: use agents array with type 'rights' instead. Rights licensing agent for this brand.",
        ),
    ] = None
    contact: Annotated[Contact1 | None, Field(description='Brand-level contact information')] = None
    collections: Annotated[
        list[Collection] | None,
        Field(
            description="Collections this person or brand is associated with. Enables bidirectional linking: a collection's talent references brand.json via brand_url, and brand.json links back to collections."
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Subclasses

  • adcp.types.generated_poc.brand_discovery.BrandDiscovery5

Class variables

var agents : adcp.types.generated_poc.brand_discovery.Agents | None
var assets : list[adcp.types.generated_poc.brand_discovery.Asset] | None
var avatar : adcp.types.generated_poc.brand_discovery.Avatar | None
var brand_agent : adcp.types.generated_poc.brand_discovery.BrandAgent | None
var collections : list[adcp.types.generated_poc.brand_discovery.Collection] | None
var colors : adcp.types.generated_poc.brand_discovery.Colors | None
var contact : adcp.types.generated_poc.brand_discovery.Contact1 | None
var data_subject_contestation : adcp.types.generated_poc.brand_discovery.DataSubjectContestation | None
var description : str | None
var disclaimers : list[adcp.types.generated_poc.brand_discovery.Disclaimer] | None
var fonts : adcp.types.generated_poc.brand_discovery.Fonts | None
var id : adcp.types.generated_poc.brand_discovery.BrandId
var identity_relying_parties : list[adcp.types.generated_poc.brand_discovery.IdentityRelyingParty] | None
var industries : list[str] | None
var keller_type : adcp.types.generated_poc.brand_discovery.KellerType | None
var logos : list[adcp.types.generated_poc.brand_discovery.Logo] | None
var model_config
var names : list[adcp.types.generated_poc.brand_discovery.LocalizedName]
var parent_brand : adcp.types.generated_poc.brand_discovery.BrandId | None
var privacy_policy_url : pydantic.networks.AnyUrl | None
var product_catalog : adcp.types.generated_poc.brand_discovery.ProductCatalog | None
var properties : list[adcp.types.generated_poc.brand_discovery.Property] | None
var rights_agent : adcp.types.generated_poc.brand_discovery.RightsAgent | None
var tagline : str | adcp.types.generated_poc.brand_discovery.Tagline | None
var target_audience : str | None
var tone : str | adcp.types.generated_poc.brand_discovery.Tone | None
var trademarks : list[adcp.types.generated_poc.brand_discovery.Trademark] | None
var url : pydantic.networks.AnyUrl | None
var visual_guidelines : adcp.types.generated_poc.brand_discovery.VisualGuidelines | None
var voice_synthesis : adcp.types.generated_poc.brand_discovery.VoiceSynthesis | None

Inherited members

class BrandActivity (**data: Any)
Expand source code
class BrandActivity(RegistryBaseModel):
    domain: Annotated[str, Field(examples=["acmecorp.com"])]
    total: Annotated[int, Field(examples=[3])]
    revisions: list[ActivityRevision]

Base model for registry API types.

Uses extra='allow' so that new fields from the registry API are preserved rather than dropped. This differs from AdCPBaseModel which defaults to extra='ignore' for protocol types.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var domain : str
var model_config
var revisions : list[ActivityRevision]
var total : int
class BrandReference (**data: Any)
Expand source code
class BrandReference(AdCPBaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    domain: Annotated[
        str,
        Field(
            description="Domain where /.well-known/brand.json is hosted, or the brand's operating domain",
            pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$',
        ),
    ]
    brand_id: Annotated[
        brand_id_1.BrandId | None,
        Field(
            description='Brand identifier within the house portfolio. Optional for single-brand domains.'
        ),
    ] = None
    countries: Annotated[
        list[Country] | None,
        Field(
            description='Canonical set of ISO 3166-1 alpha-2 countries for this advertiser identity. Omit for a global/default identity. Array order is not meaningful; producers MUST sort codes lexicographically before computing keys or signatures. This qualifies account identity and is not delivery targeting.',
            min_length=1,
        ),
    ] = None
    industries: Annotated[
        list[str] | None,
        Field(
            description="Inline override for the brand's industries. Useful when the caller cannot modify the brand's canonical brand.json but needs to declare industries for governance (e.g., Annex III vertical detection). brand.json remains the canonical source; when omitted here, governance agents SHOULD resolve from brand.json."
        ),
    ] = None
    data_subject_contestation: Annotated[
        DataSubjectContestation | None,
        Field(
            description="Inline override for the brand's contestation contact point. Useful when the operator does not control brand.json but needs to discharge Art 22(3) for this plan. brand.json is canonical; when omitted, governance agents resolve brand → house → missing."
        ),
    ] = None
    brand_kit_override: Annotated[
        BrandKitOverride | None,
        Field(
            description="Inline override for brand-kit fields normally resolved from `/.well-known/brand.json` on `domain` (logo, colors, voice, tagline). Use when brand.json is missing, stale, or inappropriate for this specific call — e.g., a campaign-scoped tagline, a co-branded creative, a freshly-rebranded color palette the brand.json hasn't shipped yet. Same inline-override pattern as `industries` and `data_subject_contestation` above: brand.json is canonical, the override is per-call. Adopters needing to override fields outside this subset (`voice_attributes`, `prohibited_terms`, etc.) MUST publish a different brand.json and reference it via a different `domain` — the inline override is intentionally narrow to a small high-traffic subset.\n\n**Merge semantics (normative).** The merge is **field-level**, not whole-object replacement. Each field within `brand_kit_override` (`logo`, `colors`, `voice`, `tagline`) is evaluated independently — when a field is present on the override the override value applies; when a field is absent the brand.json value applies (or is absent if brand.json doesn't carry one either). For composite fields (`colors.primary`, `colors.secondary`, `colors.accent`), the merge is one level deeper: each color slot is evaluated independently — a producer can override `colors.primary` while still inheriting `colors.secondary` from brand.json. SDKs MUST NOT treat a present `brand_kit_override.colors` as wiping the brand.json `colors` block entirely; only the per-slot fields present in the override take precedence. Without this rule, a partial-override semantics would diverge across SDKs and produce inconsistent rendering for the same payload."
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var brand_id : adcp.types.generated_poc.core.brand_id.BrandId | None
var brand_kit_override : adcp.types.generated_poc.core.brand_ref.BrandKitOverride | None
var countries : list[adcp.types.generated_poc.core.brand_ref.Country] | None
var data_subject_contestation : adcp.types.generated_poc.core.brand_ref.DataSubjectContestation | None
var domain : str
var industries : list[str] | None
var model_config

Inherited members

class BrandRegistryItem (**data: Any)
Expand source code
class BrandRegistryItem(RegistryBaseModel):
    domain: Annotated[str, Field(examples=["acmecorp.com"])]
    brand_name: Annotated[str | None, Field(examples=["Acme Corp"])] = None
    source: BrandRegistrySource
    has_manifest: bool
    verified: bool
    house_domain: str | None = None
    keller_type: KellerType | None = None

Base model for registry API types.

Uses extra='allow' so that new fields from the registry API are preserved rather than dropped. This differs from AdCPBaseModel which defaults to extra='ignore' for protocol types.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var brand_name : str | None
var domain : str
var has_manifest : bool
var house_domain : str | None
var keller_typeKellerType | None
var model_config
var sourceBrandRegistrySource
var verified : bool
class BrandSource (*args, **kwds)
Expand source code
class BrandSource(Enum):
    brand_json = "brand_json"
    community = "community"
    enriched = "enriched"

Create a collection of name/value pairs.

Example enumeration:

>>> class Color(Enum):
...     RED = 1
...     BLUE = 2
...     GREEN = 3

Access them by:

  • attribute access::
>>> Color.RED
<Color.RED: 1>
  • value lookup:
>>> Color(1)
<Color.RED: 1>
  • name lookup:
>>> Color['RED']
<Color.RED: 1>

Enumerations can be iterated over, and know how many members they have:

>>> len(Color)
3
>>> list(Color)
[<Color.RED: 1>, <Color.BLUE: 2>, <Color.GREEN: 3>]

Methods can be added to enumerations, and members can have their own attributes – see the documentation for details.

Ancestors

  • enum.Enum

Class variables

var brand_json
var community
var enriched
class BriefAsset (**data: Any)
Expand source code
class BriefAsset(CreativeBrief):
    asset_type: Annotated[
        Literal['brief'],
        Field(
            description='Discriminator identifying this as a brief asset. See /schemas/creative/asset-types for the registry.'
        ),
    ] = 'brief'

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.creative_brief.CreativeBrief
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var asset_type : Literal['brief']
var model_config

Inherited members

class LegacyBuildCreativeRequest (**data: Any)
Expand source code
class BuildCreativeRequest(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    governance_context: Annotated[
        str | None,
        Field(
            description='Opaque intent authorization when this creative execution incurs vendor cost.',
            max_length=4096,
            min_length=1,
            pattern='^[\\x20-\\x7E]+$',
        ),
    ] = None
    message: Annotated[
        str | None,
        Field(
            description='Natural language instructions for the transformation or generation. For pure generation, this is the creative brief. For transformation, this provides guidance on how to adapt the creative. For refinement, this describes the desired changes.'
        ),
    ] = None
    creative_manifest: Annotated[
        creative_manifest_1.CreativeManifest | None,
        Field(
            description='Creative manifest to transform or generate from. On the canonical 3.2 path it carries `format_kind`, optional `format_option_ref`, and the required input assets. For transformation (for example resizing or reformatting), this is the complete creative to adapt. When creative_id is provided, the agent resolves the creative from its library and this field is ignored.'
        ),
    ] = None
    creative_id: Annotated[
        str | None,
        Field(
            description="Reference to a creative in the agent's library. The creative agent resolves this to a manifest from its library. Use this instead of creative_manifest when retrieving an existing creative for tag generation or format adaptation."
        ),
    ] = None
    concept_id: Annotated[
        str | None,
        Field(
            description='Creative concept containing the creative. Creative agents SHOULD assign globally unique creative_id values; when they cannot guarantee uniqueness, concept_id is REQUIRED to disambiguate.'
        ),
    ] = None
    media_buy_id: Annotated[
        str | None,
        Field(
            description='Media buy identifier for tag generation context. When the creative agent is also the ad server, this provides the trafficking context needed to generate placement-specific tags (e.g., CM360 placement ID). Not needed when tags are generated at the creative level (most creative platforms).'
        ),
    ] = None
    package_id: Annotated[
        str | None,
        Field(
            description='Package identifier within the media buy. Used with media_buy_id when the creative agent needs line-item-level context for tag generation. Omit to get a tag not scoped to a specific package.'
        ),
    ] = None
    target_format_id: Annotated[
        format_id.FormatReferenceStructuredObject | None,
        Field(
            deprecated=True,
            description='**DEPRECATED in 3.2.** Legacy named-format selector. Use `target_capability_id` with a value advertised in `get_adcp_capabilities.creative.supported_formats[].capability_id`.',
        ),
    ] = None
    target_format_ids: Annotated[
        list[format_id.FormatReferenceStructuredObject] | None,
        Field(
            deprecated=True,
            description='**DEPRECATED in 3.2.** Legacy named-format selectors. Use `target_capability_ids` with values advertised in `get_adcp_capabilities.creative.supported_formats[].capability_id`.',
            max_length=50,
            min_length=1,
        ),
    ] = None
    target_capability_id: Annotated[
        str | None,
        Field(
            description='Canonical 3.2 single-output selector. Matches exactly one `get_adcp_capabilities.creative.supported_formats[].capability_id` advertised by this creative agent. The matched entry supplies the canonical `format` declaration used to validate inputs and the returned manifest. Mutually exclusive with `target_capability_ids` and the deprecated target_format_id fields.',
            pattern='^[a-zA-Z0-9_-]+$',
        ),
    ] = None
    target_capability_ids: Annotated[
        list[TargetCapabilityId] | None,
        Field(
            description='Canonical 3.2 multi-output selector. Each value matches a `get_adcp_capabilities.creative.supported_formats[].capability_id`. The creative agent produces one canonical manifest per capability in request order. Mutually exclusive with `target_capability_id` and the deprecated target_format_id fields.',
            max_length=50,
            min_length=1,
        ),
    ] = None
    transformer_id: Annotated[
        str | None,
        Field(
            description="Selects an account-scoped transformer (discovered via list_transformers) to perform the build. One transformer per call. When present, the build uses this transformer and target_capability_id/target_capability_ids select which of its outputs to produce — they MUST be a subset of the transformer's output_capability_ids. Deprecated target_format_id fields use the legacy output_format_ids compatibility path. Render configuration goes in `config`."
        ),
    ] = None
    config: Annotated[
        dict[str, Any] | None,
        Field(
            description='Typed render configuration for the selected transformer, keyed by each param\'s `field` (from the transformer\'s params[] in list_transformers). Example: { "voice": "isaac", "speaking_rate": 1.1, "mastering_preset": "podcast" }. The agent MUST validate `config` against the transformer\'s live params for this account and reject unrecognized keys and out-of-range / non-enumerated values with a field-attributed error (e.g. `config.voice`) rather than silently ignoring them — config drives a paid render. Genuinely vendor-specific or experimental knobs not declared as params belong in `ext`, not here. (The schema leaves this object open because legal keys are dynamic per transformer; strict validation is a normative agent obligation.) When `refine_from_build_variant_id` is set, `config` is applied as a DELTA over the parent leaf\'s config.'
        ),
    ] = None
    refine_from_build_variant_id: Annotated[
        str | None,
        Field(
            description='Refine a previously produced variant and return new lineage-linked variants. The transformer and target capability are inherited from the parent leaf. A refinement request MUST omit transformer_id, target_capability_id(s), and deprecated target_format_id(s); changing transformer or output format is a new transformation build, not refinement. Requires creative.supports_refinement.'
        ),
    ] = None
    mode: Annotated[
        Mode | None,
        Field(
            description="`execute` (default) produces and bills the creative(s). `estimate` is a DRY RUN: the agent produces nothing and bills nothing, and returns a BuildCreativeEstimate with a projected cost band (cost_low/cost_high) computed against THIS request's actual inputs (script length, brief, catalog size, max_creatives × max_variants) — the band the buyer cannot derive itself, since per_unit gives the rate but not the unit count. Requires the agent to advertise `creative.supports_spend_controls`; otherwise rejected with `UNSUPPORTED_FEATURE`."
        ),
    ] = Mode.execute
    max_spend: Annotated[
        MaxSpend | None,
        Field(
            description='Hard per-call spend ceiling. The agent produces leaves until the NEXT leaf would push the run\'s aggregate vendor_cost over `amount`, then STOPS and returns the partial BuildCreativeVariantSuccess produced so far with `budget_status: "capped"` (every returned leaf is real, trafficable, and billed — nothing produced is discarded; the leaf shortfall is `leaves_returned` < `leaves_total`). If even the first leaf would exceed the cap, the call fails with BUDGET_CAP_REACHED. `currency` MUST match the rate card\'s currency (the agent does not FX-convert) or the request is rejected with INVALID_REQUEST (error.field `max_spend.currency`). Requires `creative.supports_spend_controls`. Caps a SINGLE call — to bound a refinement loop, track aggregate vendor_cost across calls and stop issuing them (buyer responsibility in this revision). max_spend bounds only build-time vendor_cost: CPM-priced builds (estimate basis `cpm_deferred`) have build-time vendor_cost 0 and accrue at serve time, so max_spend never engages for them — bound a CPM fan-out with max_creatives instead.'
        ),
    ] = None
    max_creatives: Annotated[
        int | None,
        Field(
            description='Caps how many DISTINCT creatives to produce along the catalog/item fan-out axis — one creative per catalog item. Use it to sample a large catalog (e.g. send 150 job openings, set max_creatives: 5 to preview five). Distinct from item_limit, which caps how many catalog items a SINGLE creative consumes (DCO-style). Omitted with a catalog input means one creative per item up to the catalog/format bound; omitted without a catalog collapses to a single creative. Large fan-outs may return asynchronously. Mutually exclusive with `refine_from_build_variant_id` (refinement targets one prior creative, not a catalog fan-out). Supported only when the agent advertises `creative.multiplicity.supports_catalog_fanout`; values above `max_creatives_limit` are clamped. Pair with `max_spend` to bound the bill of a large fan-out.',
            ge=1,
        ),
    ] = None
    signal_conditions: Annotated[
        list[SignalCondition] | None,
        Field(
            description="Advisory keep-all PRODUCTION axis: produce one distinct creative group per signal condition, each kept and trafficked with its own signal targeting (e.g. a rain creative AND a sun creative). Sibling to max_creatives (catalog axis), NOT a variant_axis value (which is choose-among). Each item reuses SignalTargeting (value_type-discriminated binary/categorical/numeric over signal_ref) so the produced group's signal_condition resolves condition identity through the SAME schema the sales-side package targeting uses, plus an optional signal_agent_segment_id carrying the RESOLVED-segment identity (vs signal_ref's definition identity) — echo a provider-exposed handle verbatim; it is the primary trafficking-compatibility key, with categorical signal_ref+value as the weaker fallback. Per #5280 this is an ADVISORY context pointer — it informs production and MUST NOT hard-block at the build_creative layer; trafficking-compatibility (a sun creative MUST NOT serve into rain-targeted packages) is enforced reject-at-trafficking on the sales side (SIGNAL_TARGETING_INCOMPATIBLE), not here. Triggers the BuildCreativeVariantSuccess shape. Supported only when the agent advertises creative.multiplicity.supports_signal_fanout; condition counts above max_signal_conditions_limit are CLAMPED (not rejected), consistent with max_creatives. Composes with max_creatives (catalog × conditions cross-product) and max_variants (variants per group).",
            min_length=1,
        ),
    ] = None
    max_variants: Annotated[
        int | None,
        Field(
            description='Caps how many ALTERNATIVES to produce per creative (different voices, themes, best-of-N, etc.). Default 1 preserves single-output behavior. Each variant is a real, independently-billed build (you pay for all produced); the buyer keeps one or many. When variant_axis.values[] is provided, its length is authoritative over max_variants. Resolutions/quality tiers are NOT variants — request them as additional target formats.',
            ge=1,
        ),
    ] = 1
    variant_axis: Annotated[
        VariantAxis | None,
        Field(
            description='Declares the dimension along which variants differ. When `values` is provided, the agent produces exactly one variant per value (e.g. an A/B of two voices). When only `dimension` is provided, the agent chooses up to max_variants variants along that dimension (e.g. best-of-N, themes).'
        ),
    ] = None
    keep_mode: Annotated[
        KeepMode | None,
        Field(
            description='Advisory hint for how the buyer intends to use the variants. `keep_one` (best-of-N) and `keep_some` signal the agent to set `recommended`/`rank` on returned variants. Advisory only — it does not change what is returned or billed; every produced variant is returned and charged. Keeping is a client act of trafficking the chosen build_variant_id(s).'
        ),
    ] = KeepMode.keep_all
    selection_strategy: Annotated[
        creative_selection_strategy.CreativeSelectionStrategy | None,
        Field(
            description='Governs HOW the agent samples when max_creatives < items_total (folds #5262). audience_relevance draws its ranking input from the SAME signal_ref pointers in signal_conditions / package targeting — NOT a parallel signals[] array. proximity takes a location input (geo shape TBD — WG open). inventory_priority is seller-side catalog metadata (margin/overstock/promo; no buyer input). random is the status-quo default. Per-creative selection ordering surfaces on the existing rank / recommended fields of creatives[].variants[], not a new selection_rank. Advisory; absent => agent default (random).'
        ),
    ] = None
    account: Annotated[
        account_ref.AccountReference | None,
        Field(
            description='Account reference for pricing and billing. When present, the creative agent applies account-specific pricing from the rate card, records the build against the account for billing, and can enforce account-level quotas or entitlements. Required by creative agents that charge for their services.'
        ),
    ] = None
    brand: Annotated[
        brand_ref.BrandReference | None,
        Field(
            description='Brand reference for creative generation. Resolved to full brand identity (colors, logos, tone) at execution time.'
        ),
    ] = None
    quality: Annotated[
        creative_quality.CreativeQuality | None,
        Field(
            description="Quality tier for generation. 'draft' produces fast, lower-fidelity output for iteration and review. 'production' produces full-quality output for final delivery. If omitted, the creative agent uses its own default. For non-generative transforms (e.g., format resizing), creative agents MAY ignore this field."
        ),
    ] = None
    evaluator: Annotated[
        evaluator_spec.EvaluatorSpec | None,
        Field(
            description="Optional advisory evaluator (buyer-attached pointer, #5280) declaring how produced variants should be evaluated and ranked — the rank-side of the get_creative_features feature oracle. Experimental (x-status: experimental): the whole evaluator surface is new and unfrozen, and requires creative.supports_evaluator, which sellers MUST pair with `creative.evaluator` in experimental_features. Drives the producing agent's gate-then-rank pipeline over its best_of_n exploration: per leaf, evaluate (the chosen form) → optionally GATE (`evaluator.feature_requirement[]`, drop fails — internal pruning of which leaves the agent recommends, never an AdCP-layer block of an already-produced billable leaf) → RANK survivors (`evaluator.rank_by`, an explicit {feature_id, direction} ordering). Feature discovery uses get_adcp_capabilities governance.creative_features for rank_by, feature_requirement, and eval.features[]; evaluator_id is a pre-provisioned/account-arranged preset, not an ID discovered from that catalog. Populates a per-leaf `eval` block of creative-feature values (creative-feature-result[]) when supports_evaluator. When the evaluator names an external agent (`evaluator.feature_agent.agent_url` or the agent-form `agent_url`), that agent MUST appear in the seller's `creative_policy.accepted_verifiers[]` (the same allowlist #5280 established for provenance verify_agent); an off-list agent is rejected with `EVALUATOR_AGENT_NOT_ACCEPTED`. The outbound evaluator call authenticates on the transport (request signing/JWKS, mTLS, or a pre-provisioned static credential); credentials and caller-supplied trust material MUST NOT appear in evaluator, context, ext, or creative payload fields, and credential- or trust-material keys should be rejected with `CREDENTIAL_IN_ARGS`. With no `feature_requirement`, evaluation is advisory only and does not change what is produced or billed; an unreachable/unknown on-list agent degrades to seller-default ranking (advisory errors[] note), not a failure. Requires creative.supports_evaluator; otherwise ignored."
        ),
    ] = None
    item_limit: Annotated[
        int | None,
        Field(
            description="Maximum number of catalog items a SINGLE creative consumes when generating (DCO-style — e.g. how many items fill one carousel/feed creative). When a catalog asset contains more items than this limit, the creative agent selects the top items based on relevance or catalog ordering. When item_limit exceeds the format's max_items, the creative agent SHOULD use the lesser of the two. Ignored when the manifest contains no catalog assets. Distinct from `max_creatives`, which fans OUT across catalog items to produce one distinct creative per item.",
            ge=1,
        ),
    ] = None
    include_preview: Annotated[
        bool | None,
        Field(
            description="When true, requests the creative agent to include preview renders in the response alongside the manifest. Agents that support this return a 'preview' object in the response using the same structure as preview_creative. Agents that do not support inline preview simply omit the field. This avoids a separate preview_creative round trip for platforms that generate previews as a byproduct of building."
        ),
    ] = None
    preview_inputs: Annotated[
        list[PreviewInput] | None,
        Field(
            description='Input sets for preview generation when include_preview is true. Supported with a single target_capability_id; multi-capability requests generate one default preview per output. Deprecated target-format selectors retain equivalent compatibility behavior.',
            min_length=1,
        ),
    ] = None
    preview_quality: Annotated[
        creative_quality.CreativeQuality | None,
        Field(
            description="Render quality for inline preview when include_preview is true. 'draft' produces fast, lower-fidelity renderings. 'production' produces full-quality renderings. Independent of the build quality parameter — you can build at draft quality and preview at production quality, or vice versa. If omitted, the creative agent uses its own default. Ignored when include_preview is false or omitted."
        ),
    ] = None
    preview_output_format: Annotated[
        preview_output_format_1.PreviewOutputFormat | None,
        Field(
            description="Output format for preview renders when include_preview is true. 'url' returns preview_url (iframe-embeddable URL), 'html' returns preview_html (raw HTML). Ignored when include_preview is false or omitted."
        ),
    ] = preview_output_format_1.PreviewOutputFormat.url
    macro_values: Annotated[
        dict[str, str] | None,
        Field(
            description="Macro values to pre-substitute into the output manifest's assets. Keys are universal macro names (e.g., CLICK_URL, CACHEBUSTER); values are the substitution strings. The creative agent translates universal macros to its platform's native syntax. Substitution is literal — all occurrences of each macro in output assets are replaced with the provided value. The caller is responsible for URL-encoding values if the output context requires it. Macros not provided here remain as {MACRO} placeholders for the sales agent to resolve at serve time. Creative agents MUST ignore keys they do not recognize — unknown macro names are not an error."
        ),
    ] = None
    idempotency_key: Annotated[
        str,
        Field(
            description='Client-generated unique key for this request. Prevents duplicate creative generation on retries. MUST be unique per (seller, request) pair to prevent cross-seller correlation. Use a fresh UUID v4 for each request.',
            max_length=255,
            min_length=16,
            pattern='^[A-Za-z0-9_.:-]{16,255}$',
        ),
    ]
    push_notification_config: Annotated[
        push_notification_config_1.PushNotificationConfig | None,
        Field(
            description='Optional webhook configuration for async terminal completion/failure notifications on build_creative. Meaningful only when the request enters the async lifecycle and returns a Submitted envelope. Submitted envelopes with `task_id` remain pollable through `get_task_status` (legacy `tasks/get`) whether or not this field is present. If a request includes this field and the agent returns a Submitted envelope, the agent MUST deliver at least the terminal completion/failure notification to the configured URL; intermediate progress notifications are MAY. If the agent cannot honor the webhook channel, it MUST reject the request with a structured error instead of silently accepting. This field does not change response timing semantics: agents MUST NOT route a request through the async/Submitted arm or emit async delivery solely because `push_notification_config` is present; requests that can be completed inline still return the synchronous success shape.'
        ),
    ] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var account : adcp.types.generated_poc.core.account_ref.AccountReference | None
var brand : adcp.types.generated_poc.core.brand_ref.BrandReference | None
var concept_id : str | None
var config : dict[str, typing.Any] | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var creative_id : str | None
var creative_manifest : adcp.types.generated_poc.core.creative_manifest.CreativeManifest | None
var evaluator : adcp.types.generated_poc.core.evaluator_spec.EvaluatorSpec | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var governance_context : str | None
var idempotency_key : str
var include_preview : bool | None
var item_limit : int | None
var keep_mode : adcp.types.generated_poc.media_buy.build_creative_request.KeepMode | None
var macro_values : dict[str, str] | None
var max_creatives : int | None
var max_spend : adcp.types.generated_poc.media_buy.build_creative_request.MaxSpend | None
var max_variants : int | None
var media_buy_id : str | None
var message : str | None
var mode : adcp.types.generated_poc.media_buy.build_creative_request.Mode | None
var model_config
var package_id : str | None
var preview_inputs : list[adcp.types.generated_poc.media_buy.build_creative_request.PreviewInput] | None
var preview_output_format : adcp.types.generated_poc.enums.preview_output_format.PreviewOutputFormat | None
var preview_quality : adcp.types.generated_poc.enums.creative_quality.CreativeQuality | None
var push_notification_config : adcp.types.generated_poc.core.push_notification_config.PushNotificationConfig | None
var quality : adcp.types.generated_poc.enums.creative_quality.CreativeQuality | None
var refine_from_build_variant_id : str | None
var selection_strategy : adcp.types.generated_poc.enums.creative_selection_strategy.CreativeSelectionStrategy | None
var signal_conditions : list[adcp.types.generated_poc.media_buy.build_creative_request.SignalCondition] | None
var target_capability_id : str | None
var target_capability_ids : list[adcp.types.generated_poc.media_buy.build_creative_request.TargetCapabilityId] | None
var target_format_id : adcp.types.generated_poc.core.format_id.FormatReferenceStructuredObject | None
var target_format_ids : list[adcp.types.generated_poc.core.format_id.FormatReferenceStructuredObject] | None
var transformer_id : str | None
var variant_axis : adcp.types.generated_poc.media_buy.build_creative_request.VariantAxis | None

Inherited members

class LegacyBuildCreativeResponse1 (**data: Any)
Expand source code
class BuildCreativeResponse1(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    creative_manifest: creative_manifest_1.CreativeManifest
    build_variant_id: str | None = None
    recipe_hash: str | None = None
    sandbox: bool | None = None
    expires_at: AwareDatetime | None = None
    preview: Preview | None = None
    preview_error: error_1.Error | None = None
    pricing_option_id: str | None = None
    vendor_cost: Annotated[float, Field(ge=0)] | None = None
    currency: Annotated[str, StringConstraints(pattern='^[A-Z]{3}$')] | None = None
    consumption: creative_consumption_1.CreativeConsumption | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var build_variant_id : str | None
var consumption : adcp.types.generated_poc.core.creative_consumption.CreativeConsumption | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var creative_manifest : adcp.types.generated_poc.core.creative_manifest.CreativeManifest
var currency : str | None
var expires_at : pydantic.types.AwareDatetime | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config
var preview : adcp.types.generated_poc.media_buy.build_creative_response.Preview | None
var preview_error : adcp.types.generated_poc.core.error.Error | None
var pricing_option_id : str | None
var recipe_hash : str | None
var sandbox : bool | None
var vendor_cost : float | None
class LegacyBuildCreativeSuccessResponse (**data: Any)
Expand source code
class BuildCreativeResponse1(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    creative_manifest: creative_manifest_1.CreativeManifest
    build_variant_id: str | None = None
    recipe_hash: str | None = None
    sandbox: bool | None = None
    expires_at: AwareDatetime | None = None
    preview: Preview | None = None
    preview_error: error_1.Error | None = None
    pricing_option_id: str | None = None
    vendor_cost: Annotated[float, Field(ge=0)] | None = None
    currency: Annotated[str, StringConstraints(pattern='^[A-Z]{3}$')] | None = None
    consumption: creative_consumption_1.CreativeConsumption | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var build_variant_id : str | None
var consumption : adcp.types.generated_poc.core.creative_consumption.CreativeConsumption | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var creative_manifest : adcp.types.generated_poc.core.creative_manifest.CreativeManifest
var currency : str | None
var expires_at : pydantic.types.AwareDatetime | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config
var preview : adcp.types.generated_poc.media_buy.build_creative_response.Preview | None
var preview_error : adcp.types.generated_poc.core.error.Error | None
var pricing_option_id : str | None
var recipe_hash : str | None
var sandbox : bool | None
var vendor_cost : float | None

Inherited members

class LegacyBuildCreativeErrorResponse (**data: Any)
Expand source code
class BuildCreativeResponse2(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    errors: Annotated[list[error_1.Error], Field(min_length=1)]
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var errors : list[adcp.types.generated_poc.core.error.Error]
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config
class LegacyBuildCreativeResponse2 (**data: Any)
Expand source code
class BuildCreativeResponse2(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    errors: Annotated[list[error_1.Error], Field(min_length=1)]
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var errors : list[adcp.types.generated_poc.core.error.Error]
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config

Inherited members

class LegacyBuildCreativeResponse3 (**data: Any)
Expand source code
class BuildCreativeResponse3(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    creative_manifests: Annotated[list[creative_manifest_1.CreativeManifest], Field(min_length=1)]
    sandbox: bool | None = None
    expires_at: AwareDatetime | None = None
    preview: Preview3 | None = None
    preview_error: error_1.Error | None = None
    pricing_option_id: str | None = None
    vendor_cost: Annotated[float, Field(ge=0)] | None = None
    currency: Annotated[str, StringConstraints(pattern='^[A-Z]{3}$')] | None = None
    consumption: creative_consumption_1.CreativeConsumption | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var consumption : adcp.types.generated_poc.core.creative_consumption.CreativeConsumption | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var creative_manifests : list[adcp.types.generated_poc.core.creative_manifest.CreativeManifest]
var currency : str | None
var expires_at : pydantic.types.AwareDatetime | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config
var preview : adcp.types.generated_poc.media_buy.build_creative_response.Preview3 | None
var preview_error : adcp.types.generated_poc.core.error.Error | None
var pricing_option_id : str | None
var sandbox : bool | None
var vendor_cost : float | None

Inherited members

class LegacyBuildCreativeResponse4 (**data: Any)
Expand source code
class BuildCreativeResponse4(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    creatives: Annotated[list[Creative], Field(min_length=1)]
    items_total: Annotated[int, Field(ge=0)] | None = None
    items_returned: Annotated[int, Field(ge=0)] | None = None
    leaves_total: Annotated[int, Field(ge=0)] | None = None
    leaves_returned: Annotated[int, Field(ge=0)] | None = None
    vendor_cost: Annotated[float, Field(ge=0)] | None = None
    currency: Annotated[str, StringConstraints(pattern='^[A-Z]{3}$')] | None = None
    keep_mode_applied: Literal['keep_all', 'keep_one', 'keep_some'] | None = None
    selection_strategy_applied: creative_selection_strategy_1.CreativeSelectionStrategy | None = None
    budget_status: Literal['complete', 'capped'] | None = None
    errors: list[error_1.Error] | None = None
    sandbox: bool | None = None
    expires_at: AwareDatetime | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var budget_status : Literal['complete', 'capped'] | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var creatives : list[adcp.types.generated_poc.media_buy.build_creative_response.Creative]
var currency : str | None
var errors : list[adcp.types.generated_poc.core.error.Error] | None
var expires_at : pydantic.types.AwareDatetime | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var items_returned : int | None
var items_total : int | None
var keep_mode_applied : Literal['keep_all', 'keep_one', 'keep_some'] | None
var leaves_returned : int | None
var leaves_total : int | None
var model_config
var sandbox : bool | None
var selection_strategy_applied : adcp.types.generated_poc.enums.creative_selection_strategy.CreativeSelectionStrategy | None
var vendor_cost : float | None

Inherited members

class LegacyBuildCreativeResponse5 (**data: Any)
Expand source code
class BuildCreativeResponse5(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    mode: Literal['estimate'] = 'estimate'
    estimate: Estimate
    expires_at: AwareDatetime | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var estimate : adcp.types.generated_poc.media_buy.build_creative_response.Estimate
var expires_at : pydantic.types.AwareDatetime | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var mode : Literal['estimate']
var model_config

Inherited members

class LegacyBuildCreativeResponse6 (**data: Any)
Expand source code
class BuildCreativeResponse6(AdcpVersionEnvelope, ProtocolEnvelope):
    model_config = ConfigDict(extra='allow', validate_default=True)
    status: Literal[task_status_1.TaskStatus.submitted] = task_status_1.TaskStatus.submitted
    task_id: str
    message: Annotated[str, StringConstraints(max_length=2000)] | None = None
    errors: list[error_1.Error] | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • adcp.types.generated_poc.core.protocol_envelope.ProtocolEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var errors : list[adcp.types.generated_poc.core.error.Error] | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var message : str | None
var model_config
var status : Literal[<TaskStatus.submitted: 'submitted'>]
var task_id : str
class LegacyBuildCreativeSubmittedResponse (**data: Any)
Expand source code
class BuildCreativeResponse6(AdcpVersionEnvelope, ProtocolEnvelope):
    model_config = ConfigDict(extra='allow', validate_default=True)
    status: Literal[task_status_1.TaskStatus.submitted] = task_status_1.TaskStatus.submitted
    task_id: str
    message: Annotated[str, StringConstraints(max_length=2000)] | None = None
    errors: list[error_1.Error] | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • adcp.types.generated_poc.core.protocol_envelope.ProtocolEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var errors : list[adcp.types.generated_poc.core.error.Error] | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var message : str | None
var model_config
var status : Literal[<TaskStatus.submitted: 'submitted'>]
var task_id : str

Inherited members

class BuyProductsRequest (**data: Any)
Expand source code
class BuyProductsRequest(AdCPBaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    adcp_version: version_envelope.AdcpVersion | None = None
    adcp_major_version: version_envelope.AdcpMajorVersion | None = None
    idempotency_key: Annotated[
        str, Field(max_length=255, min_length=16, pattern='^[A-Za-z0-9_.:-]{16,255}$')
    ]
    account: Annotated[
        canonical_account_ref.CanonicalAccountReference,
        Field(
            description='Execution account. A natural-key account is the single brand source and MUST NOT be combined with top-level brand.'
        ),
    ]
    brand: Annotated[
        brand_key.BrandKey | None,
        Field(
            description='Brand source required when account is ID-only. Omit when account already contains brand and operator.'
        ),
    ] = None
    advertiser_industry: Annotated[
        advertiser_industry_1.AdvertiserIndustry | None,
        Field(
            description='Industry classification for this campaign. Sellers may infer it from the resolved brand manifest when omitted.'
        ),
    ] = None
    feed_version: Annotated[
        str,
        Field(
            description='list_products feed_version containing the published offers being accepted. Sellers reject stale or mismatched versions rather than silently applying changed terms.',
            min_length=1,
        ),
    ]
    pricing_version: Annotated[
        str | None,
        Field(
            description='list_products pricing_version containing the accepted rate. Buyers MUST include this whenever list_products returned one; omission means the seller does not version pricing separately.',
            min_length=1,
        ),
    ] = None
    purchases: Annotated[list[product_purchase.ProductPurchase], Field(min_length=1)]
    total_budget: TotalBudget | None = None
    daily_budget_cap: Annotated[
        float | None,
        Field(
            description="Optional hard aggregate daily spend ceiling in total_budget.currency or the media buy's derived currency. It bounds the shared daily spend pool without allocating or reserving amounts for purchases.",
            ge=0.0,
        ),
    ] = None
    budget_cap_timezone: Annotated[
        str | None,
        Field(
            description='Optional IANA timezone override shared by every aggregate and purchase daily cap. Requires buyer_timezone_override support; otherwise rejected with UNSUPPORTED_FEATURE. When omitted, budget_capping.timezone_basis selects Account.timezone or fixed_timezone.',
            min_length=1,
        ),
    ] = None
    budget_allocation: canonical_budget_allocation.CanonicalBudgetAllocation | None = None
    start_time: start_timing.StartTiming
    end_time: AwareDatetime
    pacing: pacing_1.Pacing | None = None
    bidding: bidding_policy.BiddingPolicy | None = None
    paused: bool | None = False
    purchase_order_ref: Annotated[str | None, Field(max_length=255, min_length=1)] = None
    agency_estimate_number: Annotated[str | None, Field(max_length=100)] = None
    invoice_recipient: Annotated[
        business_entity.BusinessEntity | None,
        Field(description='Authorized per-buy billing entity override.'),
    ] = None
    governance_context: Annotated[str | None, Field(max_length=4096, min_length=1)] = None
    push_notification_config: push_notification_config_1.PushNotificationConfig | None = None
    reporting_webhook: Annotated[
        reporting_webhook_1.ReportingWebhook | None,
        Field(
            description='Optional reporting delivery configuration established atomically with the MediaBuy. This is execution metadata and is not part of the immutable product pricing terms.'
        ),
    ] = None
    opportunity: Annotated[
        Opportunity | None,
        Field(
            description='Optional planning-cycle closure for a direct product purchase. Success infers closed with accepted_with_seller when status is omitted; an explicit status MUST carry that same closure.'
        ),
    ] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var account : adcp.types.generated_poc.core.canonical_account_ref.CanonicalAccountReference
var adcp_major_version : adcp.types.generated_poc.core.version_envelope.AdcpMajorVersion | None
var adcp_version : adcp.types.generated_poc.core.version_envelope.AdcpVersion | None
var advertiser_industry : adcp.types.generated_poc.enums.advertiser_industry.AdvertiserIndustry | None
var agency_estimate_number : str | None
var bidding : adcp.types.generated_poc.core.bidding_policy.BiddingPolicy | None
var brand : adcp.types.generated_poc.core.brand_key.BrandKey | None
var budget_allocation : adcp.types.generated_poc.core.canonical_budget_allocation.CanonicalBudgetAllocation | None
var budget_cap_timezone : str | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var daily_budget_cap : float | None
var end_time : pydantic.types.AwareDatetime
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var feed_version : str
var governance_context : str | None
var idempotency_key : str
var invoice_recipient : adcp.types.generated_poc.core.business_entity.BusinessEntity | None
var model_config
var opportunity : adcp.types.generated_poc.media_buy.buy_products_request.Opportunity | None
var pacing : adcp.types.generated_poc.enums.pacing.Pacing | None
var paused : bool | None
var pricing_version : str | None
var purchase_order_ref : str | None
var purchases : list[adcp.types.generated_poc.media_buy.product_purchase.ProductPurchase]
var push_notification_config : adcp.types.generated_poc.core.push_notification_config.PushNotificationConfig | None
var reporting_webhook : adcp.types.generated_poc.core.reporting_webhook.ReportingWebhook | None
var start_time : adcp.types.generated_poc.core.start_timing.StartTiming
var total_budget : adcp.types.generated_poc.media_buy.buy_products_request.TotalBudget | None

Inherited members

class BuyingMode (*args, **kwds)
Expand source code
class BuyingMode(StrEnum):
    brief = 'brief'
    wholesale = 'wholesale'
    refine = 'refine'

Enum where members are also (and must be) strings

Ancestors

  • enum.StrEnum
  • builtins.str
  • enum.ReprEnum
  • enum.Enum

Class variables

var brief
var refine
var wholesale
class CalibrateContentSuccessResponse (**data: Any)
Expand source code
class CalibrateContentResponse1(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    verdict: binary_verdict_1.BinaryVerdict
    confidence: Annotated[float, Field(ge=0, le=1)] | None = None
    explanation: str | None = None
    features: list[Feature] | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var confidence : float | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var explanation : str | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var features : list[adcp.types.generated_poc.content_standards.calibrate_content_response.Feature] | None
var model_config
var verdict : adcp.types.generated_poc.enums.binary_verdict.BinaryVerdict
class CalibrateContentResponse1 (**data: Any)
Expand source code
class CalibrateContentResponse1(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    verdict: binary_verdict_1.BinaryVerdict
    confidence: Annotated[float, Field(ge=0, le=1)] | None = None
    explanation: str | None = None
    features: list[Feature] | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var confidence : float | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var explanation : str | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var features : list[adcp.types.generated_poc.content_standards.calibrate_content_response.Feature] | None
var model_config
var verdict : adcp.types.generated_poc.enums.binary_verdict.BinaryVerdict

Inherited members

class CalibrateContentErrorResponse (**data: Any)
Expand source code
class CalibrateContentResponse2(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    errors: list[error_1.Error]
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var errors : list[adcp.types.generated_poc.core.error.Error]
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config

Inherited members

class CardAsset (**data: Any)
Expand source code
class CardAsset(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    asset_type: Annotated[
        Literal['card'],
        Field(
            description='Discriminator identifying this as a card asset. See /schemas/creative/asset-types for the registry.'
        ),
    ] = 'card'
    media: Annotated[
        asset_union.ImageAsset | asset_union.VideoAsset,
        Field(
            description="The card's primary visual asset. Either an `image` or `video` asset, matching the parent format's `allowed_card_media_asset_types` parameter.",
            discriminator='asset_type',
        ),
    ]
    headline: Annotated[
        str | None,
        Field(
            description='Optional per-card short text label (typically 25-40 chars). Length governed by `card_headline_max_chars` on the format declaration. Meta carousel headline, Pinterest pin title, Snap Collection sticker text, TikTok caption-short.'
        ),
    ] = None
    description: Annotated[
        str | None,
        Field(
            description='Optional per-card longer text (typically 100-500 chars). Distinct from `headline`: `description` is body copy, `headline` is the label. Length governed by `card_description_max_chars` on the format declaration. Meta carousel description, Pinterest pin description, AI-surface result body text, TikTok long caption.'
        ),
    ] = None
    cta: Annotated[
        str | None,
        Field(
            description="Optional per-card call-to-action label (e.g., 'SHOP_NOW', 'LEARN_MORE'). When the parent format declares `cta_values` (allowed CTA labels), the per-card `cta` MUST be one of those values. Lets a Meta or TikTok carousel show different CTAs per card."
        ),
    ] = None
    landing_page_url: Annotated[
        asset_union.UrlAsset | None,
        Field(
            description='Optional per-card click-through URL. URL asset with `url_type: "clickthrough"`.'
        ),
    ] = None
    platform_extensions: Annotated[
        list[asset_union.PlatformExtensionRef] | None,
        Field(
            description='Per-card platform-specific extensions (URI+digest references). Same hosting model as format-level platform_extensions. Use this for Meta carousel-card attributes, Pinterest pin overrides, etc. — NEVER inline non-canonical keys on the card object directly.'
        ),
    ] = None
    provenance: Annotated[
        asset_union.Provenance | None,
        Field(
            description='Provenance metadata for this card, overrides manifest-level provenance.'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var asset_type : Literal['card']
var cta : str | None
var description : str | None
var headline : str | None
var landing_page_url : adcp.types.generated_poc.core.assets.asset_union.UrlAsset | None
var media : adcp.types.generated_poc.core.assets.asset_union.ImageAsset | adcp.types.generated_poc.core.assets.asset_union.VideoAsset
var model_config
var platform_extensions : list[adcp.types.generated_poc.core.assets.asset_union.PlatformExtensionRef] | None
var provenance : adcp.types.generated_poc.core.assets.asset_union.Provenance | None

Inherited members

class Catalog (**data: Any)
Expand source code
class Catalog(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    catalog_id: Annotated[
        str | None,
        Field(
            description="Buyer's identifier for this catalog. Required when syncing via sync_catalogs. When used in creatives, references a previously synced catalog on the account."
        ),
    ] = None
    name: Annotated[
        str | None,
        Field(
            description="Human-readable name for this catalog (e.g., 'Summer Products 2025', 'Amsterdam Store Locations')."
        ),
    ] = None
    type: Annotated[
        catalog_type.CatalogType,
        Field(
            description="Catalog type. Structural types: 'offering' (AdCP Offering objects), 'product' (ecommerce entries), 'inventory' (stock per location), 'store' (physical locations), 'promotion' (deals and pricing). Vertical types: 'hotel', 'flight', 'job', 'vehicle', 'real_estate', 'education', 'destination', 'app' — each with an industry-specific item schema."
        ),
    ]
    url: Annotated[
        AnyUrl | None,
        Field(
            description="URL to an external catalog feed. The platform fetches and resolves items from this URL. For offering-type catalogs, the feed contains an array of Offering objects. For other types, the feed format is determined by feed_format. When omitted with type 'product', the platform uses its synced copy of the brand's product catalog."
        ),
    ] = None
    feed_format: Annotated[
        feed_format_1.FeedFormat | None,
        Field(
            description='Format of the external feed at url. Required when url points to a non-AdCP feed (e.g., Google Merchant Center XML, Meta Product Catalog). Omit for offering-type catalogs where the feed is native AdCP JSON.'
        ),
    ] = None
    update_frequency: Annotated[
        update_frequency_1.UpdateFrequency | None,
        Field(
            description='How often the platform should re-fetch the feed from url. Only applicable when url is provided. Platforms may use this as a hint for polling schedules.'
        ),
    ] = None
    items: Annotated[
        list[dict[str, Any]] | None,
        Field(
            description="Inline catalog data. The item schema depends on the catalog type: Offering objects for 'offering', StoreItem for 'store', HotelItem for 'hotel', FlightItem for 'flight', JobItem for 'job', VehicleItem for 'vehicle', RealEstateItem for 'real_estate', EducationItem for 'education', DestinationItem for 'destination', AppItem for 'app', or freeform objects for 'product', 'inventory', and 'promotion'. Mutually exclusive with url — provide one or the other, not both. Implementations should validate items against the type-specific schema.",
            min_length=1,
        ),
    ] = None
    ids: Annotated[
        list[str] | None,
        Field(
            description='Filter catalog to exact canonical item keys. The key field is offering_id for offering, store_id for store, hotel_id for hotel, flight_id for flight, job_id for job, vehicle_id for vehicle, listing_id for real_estate, program_id for education, destination_id for destination, and app_id for app. Product, inventory, and promotion catalogs use the stable normalized source identifier retained during ingestion (for example a retailer SKU). The same canonical key is used by catalog availability item_id.',
            min_length=1,
        ),
    ] = None
    gtins: Annotated[
        list[Gtin] | None,
        Field(
            description="Filter product-type catalogs by GTIN identifiers for cross-retailer catalog matching. Accepts standard GTIN formats (GTIN-8, UPC-A/GTIN-12, EAN-13/GTIN-13, GTIN-14). Only applicable when type is 'product'.",
            min_length=1,
        ),
    ] = None
    tags: Annotated[
        list[str] | None,
        Field(
            description='Filter catalog to items with these tags. Tags are matched using OR logic — items matching any tag are included.',
            min_length=1,
        ),
    ] = None
    category: Annotated[
        str | None,
        Field(
            description="Filter catalog to items in this category (e.g., 'beverages/soft-drinks', 'chef-positions')."
        ),
    ] = None
    query: Annotated[
        str | None,
        Field(
            description="Natural language filter for catalog items (e.g., 'all pasta sauces under $5', 'amsterdam vacancies')."
        ),
    ] = None
    conversion_events: Annotated[
        list[event_type.EventType] | None,
        Field(
            description="Event types that represent conversions for items in this catalog. Declares what events the platform should attribute to catalog items — e.g., a job catalog converts via submit_application, a product catalog via purchase. The event's content_ids field carries the item IDs that connect back to catalog items. Use content_id_type to declare what identifier type content_ids values represent.",
            min_length=1,
        ),
    ] = None
    content_id_type: Annotated[
        content_id_type_1.ContentIdType | None,
        Field(
            description="Identifier type that the event's content_ids field should be matched against for items in this catalog. For example, 'gtin' means content_ids values are Global Trade Item Numbers, 'sku' means retailer SKUs. Omit when using a custom identifier scheme not listed in the enum."
        ),
    ] = None
    feed_field_mappings: Annotated[
        list[catalog_field_mapping.CatalogFieldMapping] | None,
        Field(
            description='Declarative normalization rules for external feeds. Maps non-standard feed field names, date formats, price encodings, and image URLs to the AdCP catalog item schema. Applied during sync_catalogs ingestion. Supports field renames, named transforms (date, divide, boolean, split), static literal injection, and assignment of image URLs to typed asset pools.',
            min_length=1,
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Subclasses

  • adcp.types.generated_poc.core.assets.catalog_asset.CatalogAsset

Class variables

var catalog_id : str | None
var category : str | None
var content_id_type : adcp.types.generated_poc.enums.content_id_type.ContentIdType | None
var conversion_events : list[adcp.types.generated_poc.enums.event_type.EventType] | None
var feed_field_mappings : list[adcp.types.generated_poc.core.catalog_field_mapping.CatalogFieldMapping] | None
var feed_format : adcp.types.generated_poc.enums.feed_format.FeedFormat | None
var gtins : list[adcp.types.generated_poc.core.catalog.Gtin] | None
var ids : list[str] | None
var items : list[dict[str, typing.Any]] | None
var model_config
var name : str | None
var query : str | None
var tags : list[str] | None
var type : adcp.types.generated_poc.enums.catalog_type.CatalogType
var update_frequency : adcp.types.generated_poc.enums.update_frequency.UpdateFrequency | None
var url : pydantic.networks.AnyUrl | None
class SyncCatalogResult (**data: Any)
Expand source code
class Catalog(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    catalog_id: str
    catalog_generation: Annotated[str, StringConstraints(min_length=1, max_length=255)] | None = None
    action: catalog_action_1.CatalogAction
    platform_id: str | None = None
    item_count: Annotated[int, Field(ge=0)] | None = None
    items_approved: Annotated[int, Field(ge=0)] | None = None
    items_pending: Annotated[int, Field(ge=0)] | None = None
    items_rejected: Annotated[int, Field(ge=0)] | None = None
    item_issues: list[ItemIssue] | None = None
    last_synced_at: AwareDatetime | None = None
    next_fetch_at: AwareDatetime | None = None
    changes: list[str] | None = None
    errors: list[error_1.Error] | None = None
    warnings: list[str] | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var action : adcp.types.generated_poc.enums.catalog_action.CatalogAction
var catalog_generation : str | None
var catalog_id : str
var changes : list[str] | None
var errors : list[adcp.types.generated_poc.core.error.Error] | None
var item_count : int | None
var item_issues : list[adcp.types.generated_poc.media_buy.sync_catalogs_response.ItemIssue] | None
var items_approved : int | None
var items_pending : int | None
var items_rejected : int | None
var last_synced_at : pydantic.types.AwareDatetime | None
var model_config
var next_fetch_at : pydantic.types.AwareDatetime | None
var platform_id : str | None
var warnings : list[str] | None

Inherited members

class CatalogAction (*args, **kwds)
Expand source code
class CatalogAction(StrEnum):
    created = 'created'
    updated = 'updated'
    unchanged = 'unchanged'
    failed = 'failed'
    deleted = 'deleted'

Enum where members are also (and must be) strings

Ancestors

  • enum.StrEnum
  • builtins.str
  • enum.ReprEnum
  • enum.Enum

Class variables

var created
var deleted
var failed
var unchanged
var updated
class CatalogAsset (**data: Any)
Expand source code
class CatalogAsset(Catalog):
    asset_type: Annotated[
        Literal['catalog'],
        Field(
            description='Discriminator identifying this as a catalog asset. See /schemas/creative/asset-types for the registry.'
        ),
    ] = 'catalog'

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.catalog.Catalog
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var asset_type : Literal['catalog']
var model_config

Inherited members

class CatalogFieldBinding (root: RootModelRootType = PydanticUndefined, **data)
Expand source code
class CatalogFieldBinding(RootModel[ScalarBinding | AssetPoolBinding | CatalogFieldBinding1]):
    root: Annotated[
        ScalarBinding | AssetPoolBinding | CatalogFieldBinding1,
        Field(
            description="Maps a format template slot to a catalog item field or typed asset pool. The 'kind' field identifies the binding variant. All bindings are optional — agents can still infer mappings without them.",
            discriminator='kind',
            examples=[
                {
                    'description': 'Scalar binding — hotel name to headline slot',
                    'data': {'kind': 'scalar', 'asset_id': 'headline', 'catalog_field': 'name'},
                },
                {
                    'description': 'Scalar binding — nested field (nightly rate)',
                    'data': {
                        'kind': 'scalar',
                        'asset_id': 'price_badge',
                        'catalog_field': 'price.amount',
                    },
                },
                {
                    'description': 'Asset pool binding — hero image from landscape pool',
                    'data': {
                        'kind': 'asset_pool',
                        'asset_id': 'hero_image',
                        'asset_group_id': 'images_landscape',
                    },
                },
                {
                    'description': 'Asset pool binding — Snap vertical background from vertical pool',
                    'data': {
                        'kind': 'asset_pool',
                        'asset_id': 'snap_background',
                        'asset_group_id': 'images_vertical',
                    },
                },
                {
                    'description': 'Catalog group binding — carousel where each slide is one hotel',
                    'data': {
                        'kind': 'catalog_group',
                        'format_group_id': 'slide',
                        'catalog_item': True,
                        'per_item_bindings': [
                            {'kind': 'scalar', 'asset_id': 'title', 'catalog_field': 'name'},
                            {
                                'kind': 'scalar',
                                'asset_id': 'price',
                                'catalog_field': 'price.amount',
                            },
                            {
                                'kind': 'asset_pool',
                                'asset_id': 'image',
                                'asset_group_id': 'images_landscape',
                            },
                        ],
                    },
                },
            ],
            title='Catalog Field Binding',
        ),
    ]
    def __getattr__(self, name: str) -> Any:
        """Proxy attribute access to the wrapped type."""
        if name.startswith('_'):
            raise AttributeError(name)
        return getattr(self.root, name)

Usage Documentation

RootModel and Custom Root Types

A Pydantic BaseModel for the root object of the model.

Attributes
-----=
root
The root object of the model.
__pydantic_root_model__
Whether the model is a RootModel.
__pydantic_private__
Private fields in the model.
__pydantic_extra__
Extra fields in the model.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • pydantic.root_model.RootModel[Union[ScalarBinding, AssetPoolBinding, CatalogFieldBinding1]]
  • pydantic.root_model.RootModel
  • pydantic.main.BaseModel
  • typing.Generic

Class variables

var model_config
var root : adcp.types.generated_poc.core.requirements.catalog_field_binding.ScalarBinding | adcp.types.generated_poc.core.requirements.catalog_field_binding.AssetPoolBinding | adcp.types.generated_poc.core.requirements.catalog_field_binding.CatalogFieldBinding1
class CatalogGroupBinding (**data: Any)
Expand source code
class CatalogFieldBinding1(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    kind: Literal['catalog_group'] = 'catalog_group'
    format_group_id: Annotated[
        str,
        Field(description="The asset_group_id of a repeatable_group in the format's assets array."),
    ]
    catalog_item: Annotated[
        Literal[True],
        Field(
            description="Each repetition of the format's repeatable_group maps to one item from the catalog."
        ),
    ]
    per_item_bindings: Annotated[
        list[PerItemBindings] | None,
        Field(
            description='Scalar and asset pool bindings that apply within each repetition of the group. Nested catalog_group bindings are not permitted.',
            min_length=1,
        ),
    ] = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var catalog_item : Literal[True]
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var format_group_id : str
var kind : Literal['catalog_group']
var model_config
var per_item_bindings : list[adcp.types.generated_poc.core.requirements.catalog_field_binding.PerItemBindings] | None

Inherited members

class CatalogFieldMapping (**data: Any)
Expand source code
class CatalogFieldMapping(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    feed_field: Annotated[
        str | None,
        Field(
            description='Field name in the external feed record. Omit when injecting a static literal value (use the value property instead).'
        ),
    ] = None
    catalog_field: Annotated[
        str | None,
        Field(
            description="Target field on the catalog item schema, using dot notation for nested fields (e.g., 'name', 'price.amount', 'location.city'). Mutually exclusive with asset_group_id."
        ),
    ] = None
    asset_group_id: Annotated[
        str | None,
        Field(
            description="Places the feed field value (a URL) into a typed asset pool on the catalog item's assets array. The value is wrapped as an image or video asset in a group with this ID. Use standard group IDs: 'images_landscape', 'images_vertical', 'images_square', 'logo', 'video'. Mutually exclusive with catalog_field."
        ),
    ] = None
    value: Annotated[
        Any | None,
        Field(
            description='Static literal value to inject into catalog_field for every item, regardless of what the feed contains. Mutually exclusive with feed_field. Useful for fields the feed omits (e.g., currency when price is always USD, or a constant category value).'
        ),
    ] = None
    transform: Annotated[
        Transform | None,
        Field(
            description='Named transform to apply to the feed field value before writing to the catalog schema. See transform-specific parameters (format, timezone, by, separator).'
        ),
    ] = None
    format: Annotated[
        str | None,
        Field(
            description="For transform 'date': the input date format string (e.g., 'YYYYMMDD', 'MM/DD/YYYY', 'DD-MM-YYYY'). Output is always ISO 8601 (e.g., '2025-03-01'). Uses Unicode date pattern tokens."
        ),
    ] = None
    timezone: Annotated[
        str | None,
        Field(
            description="For transform 'date': the timezone of the input value. IANA timezone identifier (e.g., 'UTC', 'America/New_York', 'Europe/Amsterdam'). Defaults to UTC when omitted."
        ),
    ] = None
    by: Annotated[
        float | None,
        Field(
            description="For transform 'divide': the divisor to apply (e.g., 100 to convert integer cents to decimal dollars).",
            gt=0.0,
        ),
    ] = None
    separator: Annotated[
        str | None,
        Field(
            description="For transform 'split': the separator character or string to split on. Defaults to ','."
        ),
    ] = ','
    default: Annotated[
        Any | None,
        Field(
            description='Fallback value to use when feed_field is absent, null, or empty. Applied after any transform would have been applied. Allows optional feed fields to have a guaranteed baseline value.'
        ),
    ] = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var asset_group_id : str | None
var by : float | None
var catalog_field : str | None
var default : typing.Any | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var feed_field : str | None
var format : str | None
var model_config
var separator : str | None
var timezone : str | None
var transform : adcp.types.generated_poc.core.catalog_field_mapping.Transform | None
var value : typing.Any | None

Inherited members

class CatalogItemStatus (*args, **kwds)
Expand source code
class CatalogItemStatus(StrEnum):
    approved = 'approved'
    pending = 'pending'
    rejected = 'rejected'
    warning = 'warning'
    withdrawn = 'withdrawn'

Enum where members are also (and must be) strings

Ancestors

  • enum.StrEnum
  • builtins.str
  • enum.ReprEnum
  • enum.Enum

Class variables

var approved
var pending
var rejected
var warning
var withdrawn
class CatalogRequirements (**data: Any)
Expand source code
class CatalogRequirements(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    catalog_type: Annotated[
        catalog_type_1.CatalogType,
        Field(description='The catalog type this requirement applies to'),
    ]
    required: Annotated[
        bool | None,
        Field(
            description='Whether this catalog type must be present. When true, creatives using this format must reference a synced catalog of this type.'
        ),
    ] = True
    min_items: Annotated[
        int | None,
        Field(
            description='Minimum number of items the catalog must contain for this format to render properly (e.g., a carousel might require at least 3 products)',
            ge=1,
        ),
    ] = None
    max_items: Annotated[
        int | None,
        Field(
            description='Maximum number of items the format can render. Items beyond this limit are ignored. Useful for fixed-slot layouts (e.g., a 3-product card) or feed-size constraints.',
            ge=1,
        ),
    ] = None
    required_fields: Annotated[
        list[str] | None,
        Field(
            description="Fields that must be present and non-empty on every item in the catalog. Field names are catalog-type-specific (e.g., 'title', 'price', 'image_url' for product catalogs; 'store_id', 'quantity' for inventory feeds).",
            min_length=1,
        ),
    ] = None
    feed_formats: Annotated[
        list[feed_format.FeedFormat] | None,
        Field(
            description='Accepted feed formats for this catalog type. When specified, the synced catalog must use one of these formats. When omitted, any format is accepted.',
            min_length=1,
        ),
    ] = None
    offering_asset_constraints: Annotated[
        list[offering_asset_constraint.OfferingAssetConstraint] | None,
        Field(
            description="Per-item creative asset requirements. Declares what asset groups (headlines, images, videos) each catalog item must provide in its assets array, along with count bounds and per-asset technical constraints. Applicable to 'offering' and all vertical catalog types (hotel, flight, job, etc.) whose items carry typed assets.",
            min_length=1,
        ),
    ] = None
    field_bindings: Annotated[
        list[catalog_field_binding.CatalogFieldBinding] | None,
        Field(
            description='Explicit mappings from format template slots to catalog item fields or typed asset pools. Optional — creative agents can infer mappings without them, but bindings make the relationship self-describing and enable validation. Covers scalar fields (asset_id → catalog_field), asset pools (asset_id → asset_group_id on the catalog item), and repeatable groups that iterate over catalog items.',
            min_length=1,
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var catalog_type : adcp.types.generated_poc.enums.catalog_type.CatalogType
var feed_formats : list[adcp.types.generated_poc.enums.feed_format.FeedFormat] | None
var field_bindings : list[adcp.types.generated_poc.core.requirements.catalog_field_binding.CatalogFieldBinding] | None
var max_items : int | None
var min_items : int | None
var model_config
var offering_asset_constraints : list[adcp.types.generated_poc.core.requirements.offering_asset_constraint.OfferingAssetConstraint] | None
var required : bool | None
var required_fields : list[str] | None

Inherited members

class CatalogType (*args, **kwds)
Expand source code
class CatalogType(StrEnum):
    offering = 'offering'
    product = 'product'
    inventory = 'inventory'
    store = 'store'
    promotion = 'promotion'
    hotel = 'hotel'
    flight = 'flight'
    job = 'job'
    vehicle = 'vehicle'
    real_estate = 'real_estate'
    education = 'education'
    destination = 'destination'
    app = 'app'

Enum where members are also (and must be) strings

Ancestors

  • enum.StrEnum
  • builtins.str
  • enum.ReprEnum
  • enum.Enum

Class variables

var app
var destination
var education
var flight
var hotel
var inventory
var job
var offering
var product
var promotion
var real_estate
var store
var vehicle
class CheckGovernanceRequest (**data: Any)
Expand source code
class CheckGovernanceRequest3(CheckGovernanceRequest1, CheckGovernanceRequest2):
    model_config = ConfigDict(
        extra='allow',
    )
    plan_id: Annotated[
        str | None,
        Field(
            description="Campaign governance plan identifier. Required on the initial intent or availability check, before a governance_context exists. Optional on subsequent checks: the governance agent derives the plan from its own signed governance_context. If both are present, the governance agent MUST reject the request when plan_id does not match the token's plan binding. Services MUST treat governance_context as authoritative and MUST NOT require a buyer to disclose plan_id. A plan is owned by the authenticated buyer principal that synchronized it; plan_id is an identifier, not an account credential."
        ),
    ] = None
    caller: Annotated[
        AnyUrl,
        Field(
            description='Claimed URL of the agent making the request. The transport credential MUST resolve to an agent URL; the governance agent requires an exact match and uses only that resolved URL for authorization, audit, and signed context issuance. On intent checks the authenticated buyer must be the plan owner or hold an active delegation, while approved_sellers is evaluated against the target service that becomes the token audience. On execution checks the authenticated caller MUST equal that preserved audience. An unresolved body assertion never grants plan access or authorization.'
        ),
    ]
    purchase_type: Annotated[
        purchase_type_1.PurchaseType | None,
        Field(
            description="The type of financial commitment being checked. Determines which budget allocation (if any) to validate against. Defaults to 'media_buy' when omitted."
        ),
    ] = purchase_type_1.PurchaseType.media_buy
    target_agent: Annotated[
        AnyUrl | None,
        Field(
            description='Exact agent URL of the downstream service that will receive the governed task. Required on intent checks and copied byte-for-byte into the signed governance_context aud claim. This routing and authorization field is not part of payload: payload remains exactly the downstream task arguments. A consultation re-check MUST use the same target_agent.'
        ),
    ] = None
    proposed_commitment: Annotated[
        ProposedCommitment | None,
        Field(
            description='Task-neutral monetary amount the intent would authorize. For update_media_buy and control_media_buy this is the buyer-computed positive incremental commitment, not the post-update total. For accept_proposal it is derived from the supplied proposal commercial_terms; for buy_products it is derived from the purchase payload. Amount 0 explicitly represents a verified no-cost action. The governance agent persists this value as authoritative check state.'
        ),
    ] = None
    execution_commitment: Annotated[
        ExecutionCommitment | None,
        Field(
            description='Seller-computed positive incremental commitment for a MediaBuy execution check. The seller MUST derive this atomically from its authoritative proposal or current revision and the requested operation, and the governance agent MUST reject it when it exceeds the prior intent ceiling or uses another currency.'
        ),
    ] = None
    tool: Annotated[
        str | None,
        Field(
            description="The AdCP tool being checked (e.g., 'create_media_buy', 'acquire_rights', 'activate_signal'). Present on intent checks (orchestrator). The governance agent uses the presence of tool+payload to identify an intent check."
        ),
    ] = None
    payload: Annotated[
        dict[str, Any] | None,
        Field(
            description='The full downstream tool arguments exactly as they will be sent to target_agent. Present on intent checks. Governance routing metadata is carried by target_agent, never injected into this object. The governance agent can inspect any field to validate against the plan.'
        ),
    ] = None
    proposal: Annotated[
        canonical_proposal.CanonicalProposal | None,
        Field(
            description='Exact committed proposal being authorized for accept_proposal. The governance agent verifies proposal.terms_digest against commercial_terms and binds that digest into its decision state; the downstream payload carries the same digest without repeating the terms.'
        ),
    ] = None
    governance_context: Annotated[
        str | None,
        Field(
            description='Opaque authorization context from a prior approved check_governance response. Services pass it verbatim on execution and lifecycle checks; the issuing governance agent derives the plan and prior decision from the token. Intermediaries MUST NOT parse it for business logic. Governance agents MUST emit a compact JWS per the AdCP JWS profile.',
            max_length=4096,
            min_length=1,
            pattern='^[\\x20-\\x7E]+$',
        ),
    ] = None
    consultation_context: Annotated[
        str | None,
        Field(
            description='Opaque, non-authorizing handle returned with an intent conditions verdict. Pass it only when re-checking the adjusted intent so the governance agent can correlate negotiation attempts. The governance agent MUST resolve it under the authenticated principal and reject the re-check unless principal, caller, plan_id, tool, purchase_type, and target audience match the original conditions check. Services MUST NOT receive or accept this value as authorization.',
            max_length=255,
            min_length=1,
            pattern='^[A-Za-z0-9_.:-]+$',
        ),
    ] = None
    phase: Annotated[
        governance_phase.GovernancePhase | None,
        Field(
            description="The phase of an execution-shaped governed action. Ignored for intent-shaped tool+payload requests. 'purchase': initial commitment; 'modification': update to an existing commitment; 'delivery': periodic delivery reporting. Defaults to purchase if omitted. planned_delivery_1.media_buy_id is optional for purchase and required for modification/delivery."
        ),
    ] = governance_phase.GovernancePhase.purchase
    planned_delivery: Annotated[
        planned_delivery_1.PlannedDelivery | None,
        Field(description='What the seller will actually deliver. Present on execution checks.'),
    ] = None
    delivery_metrics: Annotated[
        DeliveryMetrics2 | None,
        Field(
            description="Seller-attributed canonical delivery statement. MUST be present for 'delivery' phase. The authenticated seller binds one immutable statement_id and digest to a monotonically increasing sequence; the buyer can later submit the copy it received or an independent observation through report_plan_outcome."
        ),
    ] = None
    modification_summary: Annotated[
        str | None,
        Field(
            description="Human-readable summary of what changed. SHOULD be present for 'modification' phase.",
            max_length=1000,
        ),
    ] = None
    runtime_attestations: Annotated[
        list[RuntimeAttestation1] | None,
        Field(
            description='Optional independently issued runtime evidence for an activate_signal intent check whose payload action is activate (or omitted, which defaults to activate). It MUST NOT be supplied for deactivate. Each item is the shared portable AttestationReference from the core #4529 contract; it carries no authoritative buyer-supplied decision or confidence. The governance agent MUST evaluate every item under adcp.attestations plus governance.runtime_attestations capability policy, preserve input order in response runtime_attestation_evaluations[], and reject off-policy issuers, resolvers, credential origins, and verifier nominations without network access. This field is per-check evidence outside the synced plan and therefore outside the plan_hash preimage. Other tools and purchase types cannot carry this field.',
            max_length=10,
            min_length=1,
        ),
    ] = None
    invoice_recipient: Annotated[
        business_entity.BusinessEntity | None,
        Field(
            description='Invoice recipient from the purchase request. MUST be present when the tool payload includes invoice_recipient, so the governance agent can validate billing changes.'
        ),
    ] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.governance.check_governance_request.CheckGovernanceRequest1
  • adcp.types.generated_poc.governance.check_governance_request.CheckGovernanceRequest2
  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var caller : pydantic.networks.AnyUrl
var consultation_context : str | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var delivery_metrics : adcp.types.generated_poc.governance.check_governance_request.DeliveryMetrics2 | None
var execution_commitment : adcp.types.generated_poc.governance.check_governance_request.ExecutionCommitment | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var governance_context : str | None
var invoice_recipient : adcp.types.generated_poc.core.business_entity.BusinessEntity | None
var model_config
var modification_summary : str | None
var payload : dict[str, typing.Any] | None
var phase : adcp.types.generated_poc.enums.governance_phase.GovernancePhase | None
var plan_id : str | None
var planned_delivery : adcp.types.generated_poc.core.planned_delivery.PlannedDelivery | None
var proposal : adcp.types.generated_poc.core.canonical_proposal.CanonicalProposal | None
var proposed_commitment : adcp.types.generated_poc.governance.check_governance_request.ProposedCommitment | None
var purchase_type : adcp.types.generated_poc.enums.purchase_type.PurchaseType | None
var runtime_attestations : list[adcp.types.generated_poc.governance.check_governance_request.RuntimeAttestation1] | None
var target_agent : pydantic.networks.AnyUrl | None
var tool : str | None

Inherited members

class CheckGovernanceResponse (**data: Any)
Expand source code
class CheckGovernanceResponse(AdcpVersionEnvelope):
    @model_validator(mode='before')
    @classmethod
    def _status_to_verdict(cls, data: Any) -> Any:
        if isinstance(data, dict) and 'verdict' not in data and 'status' in data:
            data = dict(data)
            data['verdict'] = data['status']
        return data

    model_config = ConfigDict(
        extra='allow',
    )
    check_id: Annotated[
        str,
        Field(
            description='Unique identifier for this governance check record. Use in report_plan_outcome to link outcomes to the check that authorized them.'
        ),
    ]
    verdict: Annotated[
        governance_decision.GovernanceDecision,
        Field(
            description='Governance verdict: approved | denied | conditions. Renamed from `status` in 3.1 to free the top-level `status` key for the envelope task-status (TaskStatus) under MCP flat-on-the-wire serialization. The enum values are unchanged; only the property name moved.'
        ),
    ]
    check_type: Annotated[
        CheckType | None,
        Field(
            description='Check shape that produced the verdict. Required for the cross-role governance_enforcement contract. Its presence selects the modern verdict-specific response rules; its absence selects the deprecated legacy 3.x compatibility shape. Intent checks may return conditions; execution checks are binary approved or denied.'
        ),
    ] = None
    plan_id: Annotated[
        str | None,
        Field(
            description='Plan identifier echoed on an initial plan-addressed check. Optional on continuation checks addressed by governance_context; services do not need this value and MUST treat the token binding as authoritative.'
        ),
    ] = None
    explanation: Annotated[
        str, Field(description='Human-readable explanation of the governance decision.')
    ]
    findings: Annotated[
        list[Finding] | None,
        Field(
            description="Specific issues found during the governance check. Present when verdict is 'denied' or 'conditions'. MAY also be present on 'approved' for informational findings (e.g., budget approaching limit)."
        ),
    ] = None
    conditions: Annotated[
        list[Condition] | None,
        Field(
            description="Intent-phase counterproposal. Present only when verdict is 'conditions'. It does not authorize execution and MUST NOT be returned for execution or lifecycle checks. Each field path is rooted at the complete check_governance request arguments, so both payload.* and proposed_commitment.* can be addressed. After applying conditions, the caller MUST re-call check_governance with the adjusted parameters and receive approved before proceeding."
        ),
    ] = None
    consultation_context: Annotated[
        str | None,
        Field(
            description='Opaque negotiation handle present only with modern conditions responses. It carries no authorization and MUST NOT be sent to a downstream service. The governance agent MUST bind it server-side to the authenticated principal, caller, plan, tool, purchase type, and target audience, and reject a re-check if any binding changes. The buyer returns it only on the adjusted intent re-check.',
            max_length=255,
            min_length=1,
            pattern='^[A-Za-z0-9_.:-]+$',
        ),
    ] = None
    expires_at: Annotated[
        AwareDatetime | None,
        Field(
            description="When this approval expires. In the cross-role shape, present only when verdict is 'approved'. Deprecated legacy conditions responses may also carry it for 3.x compatibility. The caller must act before this time or re-call check_governance. A lapsed approval is no approval."
        ),
    ] = None
    next_check: Annotated[
        AwareDatetime | None,
        Field(
            description='When the seller should next call check_governance with delivery metrics. Present when the governance agent expects ongoing delivery reporting.'
        ),
    ] = None
    delivery_statement: Annotated[
        DeliveryStatement | None,
        Field(
            description='Canonical seller-attributed delivery statement retained by governance. Present on delivery execution checks. The buyer binds any later observation to this exact statement through report_plan_outcome.'
        ),
    ] = None
    categories_evaluated: Annotated[
        list[str] | None,
        Field(
            description="Governance categories evaluated during this check. Each value is an **agent-internal** label (e.g., `budget_authority`, `regulatory_compliance`, or any internal-reviewer key the agent's policy model defines) — not a protocol-level enum. Since one governance agent per account composes all specialist review behind its single endpoint, `categories_evaluated` is how that internal decomposition surfaces to auditors. Consumers MUST treat values as opaque labels for display and audit, not as a machine-level contract."
        ),
    ] = None
    policies_evaluated: Annotated[
        list[str] | None,
        Field(
            description="Policy IDs evaluated during this check. Includes registry policy IDs (resolved via the policy registry) and any inline `policy_id`s declared in the plan's `custom_policies`."
        ),
    ] = None
    mode: Annotated[
        governance_mode.GovernanceMode | None,
        Field(
            description='Governance enforcement mode active when this check was evaluated. Allows counterparties, regulators, and auditors to distinguish whether a finding blocked execution (enforce) or was logged silently (audit).'
        ),
    ] = None
    runtime_attestation_evaluations: Annotated[
        list[RuntimeAttestationEvaluation] | None,
        Field(
            description="Evaluator-of-record results for request runtime_attestations[], in the same order and with exactly one result per presentation. Each result is the shared AttestationEvaluation and MUST bind to this response's check_id through action_binding.action_type = https://adcontextprotocol.org/actions/governance-check and action_binding.action_id = check_id. The signed governance_context MUST bind the same reference_digest/outcome pairs; large evidence stays in the audit log rather than the token.",
            max_length=10,
            min_length=1,
        ),
    ] = None
    runtime_attestation_binding_digest: Annotated[
        str | None,
        Field(
            description='SHA-256 of RFC 8785 JCS({ evaluations: runtime_attestation_evaluations, findings: attestation_bound_findings }), where attestation_bound_findings is the response findings[] subset carrying attestation_reference_digest, preserved in response order. Required whenever runtime_attestation_evaluations is present. The governance_context JWS carries this exact value as runtime_attestation_binding_digest; get_plan_audit_logs retains ordered {reference, evaluation} pairs so auditors can first recompute every reference_digest and then prove which evaluations and findings the signed decision relied on.',
            pattern='^sha256:[a-f0-9]{64}$',
        ),
    ] = None
    governance_context: Annotated[
        str | None,
        Field(
            description='Opaque authorization context for this governed action. Present only when verdict is approved; denied and conditions responses MUST NOT carry it. The buyer attaches it to the protocol envelope when sending the governed request. The service persists and forwards it on subsequent execution and lifecycle checks without requiring plan_id.\n\nGovernance agents MUST emit a compact JWS per the AdCP JWS profile. Verifiers validate the standard authorization claims but MUST NOT interpret embedded governance state for business logic. The issuing governance agent uses the token to recover its internal plan and decision state.',
            max_length=4096,
            min_length=1,
            pattern='^[\\x20-\\x7E]+$',
        ),
    ] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var categories_evaluated : list[str] | None
var check_id : str
var check_type : adcp.types.generated_poc.governance.check_governance_response.CheckType | None
var conditions : list[adcp.types.generated_poc.governance.check_governance_response.Condition] | None
var consultation_context : str | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var delivery_statement : adcp.types.generated_poc.governance.check_governance_response.DeliveryStatement | None
var expires_at : pydantic.types.AwareDatetime | None
var explanation : str
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var findings : list[adcp.types.generated_poc.governance.check_governance_response.Finding] | None
var governance_context : str | None
var mode : adcp.types.generated_poc.enums.governance_mode.GovernanceMode | None
var model_config
var next_check : pydantic.types.AwareDatetime | None
var plan_id : str | None
var policies_evaluated : list[str] | None
var runtime_attestation_binding_digest : str | None
var runtime_attestation_evaluations : list[adcp.types.generated_poc.governance.check_governance_response.RuntimeAttestationEvaluation] | None
var verdict : adcp.types.generated_poc.enums.governance_decision.GovernanceDecision

Inherited members

class Checkpoint (*args, **kwargs)
Expand source code
class Checkpoint(TypedDict):
    """Persistable session-resume state for an A2A ``ADCPClient``.

    The minimal set of fields needed to reconnect to an in-flight A2A
    conversation after a process restart. Produced by
    ``ADCPClient.checkpoint()``; consumed by
    ``ADCPClient.from_checkpoint()``.

    - ``agent_id`` — binds the checkpoint to the agent that minted it,
      so a restore against the wrong ``AgentConfig`` fails loudly
      instead of sending Agent A's ids to Agent B.
    - ``context_id`` — the A2A conversation id.
    - ``active_task_id`` — the in-flight task the next message must
      echo; ``None`` if no task is pending.
    """

    agent_id: str
    context_id: str | None
    active_task_id: str | None

Persistable session-resume state for an A2A ADCPClient.

The minimal set of fields needed to reconnect to an in-flight A2A conversation after a process restart. Produced by ADCPClient.checkpoint(); consumed by ADCPClient.from_checkpoint().

  • agent_id — binds the checkpoint to the agent that minted it, so a restore against the wrong AgentConfig fails loudly instead of sending Agent A's ids to Agent B.
  • context_id — the A2A conversation id.
  • active_task_id — the in-flight task the next message must echo; None if no task is pending.

Ancestors

  • builtins.dict

Class variables

var active_task_id : str | None
var agent_id : str
var context_id : str | None
class CompatibilityPurchaseCoordinatorInput (**data: Any)
Expand source code
class CompatibilityPurchaseCoordinatorInput(AdCPBaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    idempotency_key: Annotated[
        UUID,
        Field(
            description='Replay identity for this logical coordinator operation. Exact retries resume the durable operation record instead of redeeming the continuation again.'
        ),
    ]
    continuation_token: Annotated[
        str,
        Field(
            description='Opaque token returned by products_available.purchase_continuation.',
            min_length=16,
        ),
    ]
    account: Annotated[
        account_ref.AccountReference,
        Field(
            description='Account identity that must match the account bound into the continuation token.'
        ),
    ]
    selected_product_ids: Annotated[
        list[SelectedProductId],
        Field(
            description='Non-empty subset of the product IDs bound into the continuation.',
            min_length=1,
            json_schema_extra={'uniqueItems': True},
        ),
    ]
    accepted_losses: Annotated[
        list[AcceptedLoss],
        Field(
            description='Exact loss set returned with the continuation. Missing, extra, or stale consent fails before mutation.',
            min_length=2,
            json_schema_extra={
                'uniqueItems': True,
                'allOf': [
                    {'contains': {'const': 'feed_version_not_atomic'}},
                    {'contains': {'const': 'pricing_version_not_atomic'}},
                ],
            },
        ),
    ]
    legacy_create_request: Annotated[
        dict[str, Any],
        Field(
            description='Proposed create_media_buy payload. Before mutation the coordinator validates this object against create-media-buy-request.json from source_adcp_version, requires explicit-package mode, and requires its package product IDs to equal selected_product_ids.',
            min_length=1,
        ),
    ]


    @field_validator('selected_product_ids')
    @classmethod
    def _selected_product_ids_are_unique(
        cls, values: list[SelectedProductId]
    ) -> list[SelectedProductId]:
        if len(values) != len({value.root for value in values}):
            raise ValueError('selected_product_ids must contain unique items')
        return values

    @field_validator('accepted_losses')
    @classmethod
    def _accepted_losses_match_schema(
        cls, values: list[AcceptedLoss]
    ) -> list[AcceptedLoss]:
        value_set = set(values)
        if len(values) != len(value_set):
            raise ValueError('accepted_losses must contain unique items')
        required = {
            AcceptedLoss.feed_version_not_atomic,
            AcceptedLoss.pricing_version_not_atomic,
        }
        if not required.issubset(value_set):
            raise ValueError('accepted_losses must include the required compatibility losses')
        return values

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var accepted_losses : list[adcp.types.generated_poc.media_buy.legacy_purchase_continuation_input.AcceptedLoss]
var account : adcp.types.generated_poc.core.account_ref.AccountReference
var continuation_token : str
var idempotency_key : uuid.UUID
var legacy_create_request : dict[str, typing.Any]
var model_config
var selected_product_ids : list[adcp.types.generated_poc.media_buy.legacy_purchase_continuation_input.SelectedProductId]

Inherited members

class ComplyTestControllerRequest (**data: Any)
Expand source code
class ComplyTestControllerRequest(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    scenario: Annotated[
        str,
        Field(
            description="Test scenario to execute. 'list_scenarios' discovers supported scenarios. 'force_*' and 'simulate_*' trigger state transitions. 'catalog_item_availability_probe' provides deterministic sandbox operations for cross-principal reference seeding, actual eligibility observation, clock advancement, and catalog delete/recreate generation tests. 'compact_product_lifecycle_probe' prepares deterministic synchronous compact proposal, acceptance, operational-control, and MediaBuy readback behavior and expires a committed proposal strictly after its hold deadline. 'compact_direct_buy_lifecycle_probe' prepares deterministic synchronous list, direct-purchase, operational-control, and readback behavior for a published product. 'force_creative_purge' destroys or tombstones a sandbox creative so account-level `creative.purged` webhooks can be observed where the seller supports the lifecycle surface. 'force_create_media_buy_arm', 'force_get_products_arm', and 'force_get_signals_arm' register one-shot response-arm directives for the next matching operation from the caller's authenticated sandbox account + principal pair. 'seed_*' scenarios pre-populate fixtures (account, product, pricing option, creative, plan, media buy, rights grant, creative format, measurement catalog) so storyboards can reference fixture IDs and external-catalog facts without implementers guessing which fixtures the conformance suite expects. 'query_upstream_traffic' returns outbound HTTP calls the agent has made since session start (or since a caller-supplied timestamp), so storyboard runners can assert upstream side-effects via `check: upstream_traffic`. 'query_provenance_audit_observations' returns sandbox-only audit observations recorded for a submitted creative so storyboards can assert non-blocking governance observations without exposing an internal audit log on public seller responses. 'force_upstream_unavailable' marks a named upstream dependency as unreachable for the duration of the compliance session (or until the seller resets it), so storyboards can exercise stale-cache fallback paths - see the `stale_response_advisory` universal storyboard. The contract raises the bar against unintentional facades - adapters that satisfy AdCP schema requirements with synthetic placeholders. It is NOT an adversarial integrity check: adopters self-report their own traffic. Adopters MUST scope the response to traffic caused by the requesting principal's session/auth context - cross-caller traffic MUST NOT be returned, regardless of the supplied since_timestamp. Multi-tenant sandboxes MUST key the recording buffer on the comply_test_controller invocation's auth principal. Runners and sellers MUST accept unknown scenario strings - new scenarios may be added in additive releases."
        ),
    ]
    params: Annotated[
        Params | None,
        Field(
            description='Scenario-specific parameters. Required for all scenarios except list_scenarios.'
        ),
    ] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None
    account: Annotated[
        Account | None,
        Field(
            description="Sandbox account assertion. The runner MUST set sandbox: true on every comply_test_controller request. The seller MUST refuse the request (returning a structured error) if the targeted account is not a sandbox account in the seller's persisted records. This field is a caller-side declaration of intent — it does not grant sandbox status; sellers verify against their own account state. The (Sandbox) verification tier is defined by this gate: real production endpoints accept sandbox-flagged traffic and process it without real-world side effects, no separate test-mode endpoint required. See spec issue #3755 and the (Sandbox) framing in #4379."
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var account : adcp.types.generated_poc.compliance.comply_test_controller_request.Account | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config
var params : adcp.types.generated_poc.compliance.comply_test_controller_request.Params | None
var scenario : str

Inherited members

class ComplyTestControllerResponse (**data: Any)
Expand source code
class ComplyTestControllerResponse(AdcpVersionEnvelope, ProtocolEnvelope):
    pass

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • adcp.types.generated_poc.core.protocol_envelope.ProtocolEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var model_config
class ComplyTestControllerResponse1 (**data: Any)
Expand source code
class ComplyTestControllerResponse(AdcpVersionEnvelope, ProtocolEnvelope):
    pass

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • adcp.types.generated_poc.core.protocol_envelope.ProtocolEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var model_config

Inherited members

class ConfigurationError (message: str,
agent_id: str | None = None,
agent_uri: str | None = None,
suggestion: str | None = None)
Expand source code
class ConfigurationError(ADCPError):
    """Invalid SDK configuration detected at construction time.

    Raised when a value passed to a client/server constructor cannot
    be reconciled with the SDK's compile-time pin — most commonly a
    cross-major ``adcp_version`` (e.g. ``adcp_version="4.0"`` against
    an SDK built for AdCP 3.x), or an unparseable version string.

    Recovery: install the SDK major that targets the wire version you
    want to speak. Cross-major pinning is not supported within a
    single SDK major.
    """

Invalid SDK configuration detected at construction time.

Raised when a value passed to a client/server constructor cannot be reconciled with the SDK's compile-time pin — most commonly a cross-major adcp_version (e.g. adcp_version="4.0" against an SDK built for AdCP 3.x), or an unparseable version string.

Recovery: install the SDK major that targets the wire version you want to speak. Cross-major pinning is not supported within a single SDK major.

Initialize exception with context.

Ancestors

  • ADCPError
  • builtins.Exception
  • builtins.BaseException

Inherited members

class ConsentBasis (*args, **kwds)
Expand source code
class ConsentBasis(StrEnum):
    consent = 'consent'
    legitimate_interest = 'legitimate_interest'
    contract = 'contract'
    legal_obligation = 'legal_obligation'

Enum where members are also (and must be) strings

Ancestors

  • enum.StrEnum
  • builtins.str
  • enum.ReprEnum
  • enum.Enum

Class variables

var consent
var contract
var legal_obligation
var legitimate_interest
class ContentIdType (*args, **kwds)
Expand source code
class ContentIdType(StrEnum):
    sku = 'sku'
    gtin = 'gtin'
    offering_id = 'offering_id'
    job_id = 'job_id'
    hotel_id = 'hotel_id'
    flight_id = 'flight_id'
    vehicle_id = 'vehicle_id'
    listing_id = 'listing_id'
    store_id = 'store_id'
    program_id = 'program_id'
    destination_id = 'destination_id'
    app_id = 'app_id'

Enum where members are also (and must be) strings

Ancestors

  • enum.StrEnum
  • builtins.str
  • enum.ReprEnum
  • enum.Enum

Class variables

var app_id
var destination_id
var flight_id
var gtin
var hotel_id
var job_id
var listing_id
var offering_id
var program_id
var sku
var store_id
var vehicle_id
class ContextMatchRequest (**data: Any)
Expand source code
class ContextMatchRequest(AdCPBaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    field_schema: Annotated[
        AnyUrl | None,
        Field(
            alias='$schema', description='Optional schema URI for validation. Ignored at runtime.'
        ),
    ] = None
    adcp_version: Annotated[
        str | None,
        Field(
            description='Release-precision AdCP version (VERSION.RELEASE, e.g. "3.0", "3.1", "3.1-beta"). On a request: the buyer\'s release pin. Inlined here (rather than via core/version-envelope.json allOf) so this schema can keep `additionalProperties: false` — the privacy boundary on this endpoint is contract-bearing.',
            pattern='^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[a-zA-Z0-9](?:[a-zA-Z0-9.-]*[a-zA-Z0-9])?)?$',
        ),
    ] = None
    adcp_major_version: Annotated[
        int | None,
        Field(
            deprecated=True,
            description='DEPRECATED in favor of adcp_version. Removed in 4.0. Inlined alongside adcp_version to preserve strict-mode on this endpoint.',
            ge=1,
            le=99,
        ),
    ] = None
    type: Annotated[
        Literal['context_match_request'],
        Field(description='Message type discriminator for deserialization.'),
    ] = 'context_match_request'
    protocol_version: Annotated[
        str | None,
        Field(
            description='TMP protocol version. Allows receivers to handle semantic differences across versions.'
        ),
    ] = '1.0'
    request_id: Annotated[
        str,
        Field(
            description='Unique request identifier. MUST NOT correlate with any identity match request_id.'
        ),
    ]
    property_rid: Annotated[
        UUID,
        Field(
            description='Property catalog UUID (UUID v7). Globally unique, stable identifier assigned by the property catalog. The primary key for TMP matching and property list targeting.'
        ),
    ]
    property_id: Annotated[
        property_id_1.PropertyId | None,
        Field(
            description="Publisher's human-readable property slug (e.g., 'cnn_homepage'). Optional when property_rid is present. Useful for logging and debugging."
        ),
    ] = None
    property_type: Annotated[
        property_type_1.PropertyType, Field(description='Type of the publisher property')
    ]
    placement_id: Annotated[
        str,
        Field(
            description="Placement identifier from the publisher's placement registry in adagents.json. Identifies where on the property this ad opportunity exists. One placement per request."
        ),
    ]
    seller_agent_url: Annotated[
        AnyUrl,
        Field(
            description="API endpoint URL of the seller agent issuing this request. The provider uses this to resolve the active package set it has synced for this seller; when `package_ids` is omitted, evaluation occurs against that full set. If `seller_agent_url` does not match any seller the provider has synced packages for, the provider MUST return an empty offer set — it MUST NOT fall back to another seller's active set. The value identifies the asking seller, is identical for every user on a given placement, and carries no user identity, so it neither varies the request per user nor weakens the context/identity decorrelation boundary. Compared using the AdCP URL canonicalization rules, not byte-equality — see docs/reference/url-canonicalization. Consistent with `seller_agent_url` on the identity match request, `seller_agent.agent_url` on `AvailablePackage`, and `agent_url` in `adagents.json`."
        ),
    ]
    artifact: Annotated[
        artifact_1.Artifact | None,
        Field(
            description='Full content artifact adjacent to this ad opportunity. Same schema used for content standards evaluation. The publisher sends the artifact when they want the buyer to evaluate the full content. Contractual protections govern buyer use. TEE deployment upgrades contractual trust to cryptographic verification. Because the router fans out to multiple buyer agents, publishers MUST NOT include bearer tokens, service-account credentials, or signed URLs in this artifact. Routers MUST remove every asset `access` object and remove or replace every credential-bearing asset `url` before forwarding; only public asset URLs that recipients can resolve independently may remain.'
        ),
    ] = None
    artifact_refs: Annotated[
        list[ArtifactRef] | None,
        Field(
            description='Public content references adjacent to this ad opportunity. Each artifact identifies content via a public identifier the buyer can resolve independently — no private registry sync required.',
            max_length=20,
            min_length=1,
        ),
    ] = None
    geo: Annotated[
        Geo | None,
        Field(
            description='Coarse geographic location of the viewer. Publisher controls granularity — country is sufficient for regulatory compliance and volume filtering, region or metro helps with campaign targeting and valuation. Coarsened to prevent user identification: no postcode, no coordinates. All fields optional.'
        ),
    ] = None
    context_signals: Annotated[
        ContextSignals | None,
        Field(
            description='Pre-computed classifier outputs for the content environment. Use when the publisher wants to provide classified context without sharing content or public references. Can supplement artifact_refs (e.g., URL + pre-classified topics) or replace them entirely (e.g., ephemeral conversation turns). Raw content MUST NOT be included — only classified outputs. The publisher is the classifier boundary.'
        ),
    ] = None
    package_ids: Annotated[
        list[str] | None,
        Field(
            description='Restrict evaluation to specific packages. When omitted, the provider evaluates all eligible packages for this placement (the common case). MUST NOT vary by user — the same package_ids must be sent for every user on a given placement. User-dependent filtering leaks identity into the context path.',
            max_length=500,
            min_length=1,
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var adcp_major_version : int | None
var adcp_version : str | None
var artifact : adcp.types.generated_poc.content_standards.artifact.Artifact | None
var artifact_refs : list[adcp.types.generated_poc.trusted_match.context_match_request.ArtifactRef] | None
var context_signals : adcp.types.generated_poc.trusted_match.context_match_request.ContextSignals | None
var field_schema : pydantic.networks.AnyUrl | None
var geo : adcp.types.generated_poc.trusted_match.context_match_request.Geo | None
var model_config
var package_ids : list[str] | None
var placement_id : str
var property_id : adcp.types.generated_poc.core.property_id.PropertyId | None
var property_rid : uuid.UUID
var property_type : adcp.types.generated_poc.enums.property_type.PropertyType
var protocol_version : str | None
var request_id : str
var seller_agent_url : pydantic.networks.AnyUrl
var type : Literal['context_match_request']

Inherited members

class ContextMatchResponse (**data: Any)
Expand source code
class ContextMatchResponseRouterPublisher(AdcpVersionEnvelope, ProtocolEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    type: Annotated[
        Literal['context_match_response'],
        Field(description='Message type discriminator for deserialization.'),
    ] = 'context_match_response'
    request_id: Annotated[
        str, Field(description='Echoed request identifier from the context match request.')
    ]
    offers: Annotated[
        list[offer.Offer],
        Field(
            description='Offers collected across the provider fan-out, one per activated package. An empty array means no packages matched. For simple activation, each offer has just package_id. For richer responses, offers include brand, price, summary, and creative manifest.'
        ),
    ]
    signals: Annotated[
        Signals | None,
        Field(
            description='Merged non-keyed response-level signals. Provider-local targeting pairs do not pass through this object; the router emits them only in signals_by_provider.'
        ),
    ] = None
    signals_by_provider: Annotated[
        dict[Annotated[str, StringConstraints(pattern=r'^[A-Za-z0-9_]+$', min_length=1, max_length=64)], SignalsByProvider]
        | None,
        Field(
            description="Router-authored map of provider targeting pairs, keyed by the publisher-assigned provider_id from provider registration. For every provider response containing a non-empty signals.targeting_kvs list, the router copies the complete list unchanged into that provider's bucket. The router derives the map key from its registration and MUST ignore or reject provider-supplied signals_by_provider data. A provider with no targeting pairs is omitted. Publishers resolve each (provider_id, key) tuple to a local ad-server destination and drop tuples that have no local mapping."
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • adcp.types.generated_poc.core.protocol_envelope.ProtocolEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var model_config
var offers : list[adcp.types.generated_poc.trusted_match.offer.Offer]
var request_id : str
var signals : adcp.types.generated_poc.trusted_match.context_match_response.Signals | None
var signals_by_provider : dict[str, adcp.types.generated_poc.trusted_match.context_match_response.SignalsByProvider] | None
var type : Literal['context_match_response']

Inherited members

class ContextObject (**data: Any)
Expand source code
class ContextObject(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var model_config

Inherited members

class ControlMediaBuyRequest (**data: Any)
Expand source code
class ControlMediaBuyRequest(AdCPBaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    adcp_version: version_envelope.AdcpVersion | None = None
    adcp_major_version: version_envelope.AdcpMajorVersion | None = None
    idempotency_key: Annotated[
        str, Field(max_length=255, min_length=16, pattern='^[A-Za-z0-9_.:-]{16,255}$')
    ]
    account: canonical_account_ref.CanonicalAccountReference
    media_buy_id: Annotated[str, Field(min_length=1)]
    revision: Annotated[
        int,
        Field(
            description='Required optimistic-concurrency revision from the latest MediaBuy snapshot.',
            ge=1,
        ),
    ]
    name: Annotated[
        str | None,
        Field(
            description='Replace the human-readable MediaBuy name as revision-checked operational metadata. This display label is not an identifier, financial reference, or change to the accepted commercial terms.',
            max_length=255,
            min_length=1,
            pattern='\\S',
        ),
    ] = None
    paused: bool | None = None
    canceled: Annotated[
        Literal[True],
        Field(
            description='Exercise an already-accepted unilateral cancellation right. A cancellation requiring seller agreement is requested by refining the accepted proposal.'
        ),
    ] = True
    cancellation_reason: Annotated[str | None, Field(max_length=500, min_length=1)] = None
    total_budget: TotalBudget | None = None
    daily_budget_cap: Annotated[
        float | None,
        Field(
            description='Replace the hard aggregate daily cap; null removes it. Numeric changes apply immediately with current-cap-day spend counted and do not redistribute purchase caps.',
            ge=0.0,
        ),
    ] = None
    budget_cap_timezone: Annotated[
        str | None,
        Field(
            description='Replace the shared IANA cap-day timezone override; null restores the default selected by budget_capping.timezone_basis (Account.timezone or fixed_timezone). A timezone change begins at the next boundary under the previously effective timezone.',
            min_length=1,
        ),
    ] = None
    budget_allocation: canonical_budget_allocation.CanonicalBudgetAllocation | None = None
    pacing: pacing_1.Pacing | None = None
    bidding: bidding_policy.BiddingPolicy | None = None
    packages: Annotated[
        list[package_control.PackageControl] | None,
        Field(
            description='Operational patches keyed by package_id. Each package_id MUST appear at most once; sellers reject duplicate IDs atomically.',
            min_length=1,
        ),
    ] = None
    reporting_webhook: reporting_webhook_1.ReportingWebhook | None = None
    governance_context: Annotated[str | None, Field(max_length=4096, min_length=1)] = None
    push_notification_config: push_notification_config_1.PushNotificationConfig | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var account : adcp.types.generated_poc.core.canonical_account_ref.CanonicalAccountReference
var adcp_major_version : adcp.types.generated_poc.core.version_envelope.AdcpMajorVersion | None
var adcp_version : adcp.types.generated_poc.core.version_envelope.AdcpVersion | None
var bidding : adcp.types.generated_poc.core.bidding_policy.BiddingPolicy | None
var budget_allocation : adcp.types.generated_poc.core.canonical_budget_allocation.CanonicalBudgetAllocation | None
var budget_cap_timezone : str | None
var canceled : Literal[True]
var cancellation_reason : str | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var daily_budget_cap : float | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var governance_context : str | None
var idempotency_key : str
var media_buy_id : str
var model_config
var name : str | None
var pacing : adcp.types.generated_poc.enums.pacing.Pacing | None
var packages : list[adcp.types.generated_poc.media_buy.package_control.PackageControl] | None
var paused : bool | None
var push_notification_config : adcp.types.generated_poc.core.push_notification_config.PushNotificationConfig | None
var reporting_webhook : adcp.types.generated_poc.core.reporting_webhook.ReportingWebhook | None
var revision : int
var total_budget : adcp.types.generated_poc.media_buy.control_media_buy_request.TotalBudget | None

Inherited members

class CpaPricingOption (**data: Any)
Expand source code
class CpaPricingOption(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    pricing_option_id: Annotated[
        str, Field(description='Unique identifier for this pricing option within the product')
    ]
    pricing_model: Annotated[
        Literal['cpa'], Field(description='Cost per acquisition (conversion event)')
    ] = 'cpa'
    event_type: Annotated[
        event_type_1.EventType,
        Field(
            description='The conversion event type that triggers billing (e.g., purchase, lead, app_install)'
        ),
    ]
    custom_event_name: Annotated[
        str | None,
        Field(
            description="Name of the custom event when event_type is 'custom'. Required when event_type is 'custom', ignored otherwise."
        ),
    ] = None
    event_source_id: Annotated[
        str | None,
        Field(
            description='When present, only events from this specific event source count toward billing. Allows different CPA rates for different sources (e.g., online vs in-store purchases). Must match an event source configured via sync_event_sources.'
        ),
    ] = None
    currency: Annotated[
        str,
        Field(
            description='ISO 4217 currency code',
            examples=['USD', 'EUR', 'GBP', 'JPY'],
            pattern='^[A-Z]{3}$',
        ),
    ]
    fixed_price: Annotated[
        float, Field(description='Fixed price per acquisition in the specified currency', gt=0.0)
    ]
    min_spend_per_package: Annotated[
        float | None,
        Field(
            description='Minimum spend requirement per package using this pricing option, in the specified currency',
            ge=0.0,
        ),
    ] = None
    price_breakdown: Annotated[
        price_breakdown_1.PriceBreakdown | None,
        Field(
            description='Breakdown of how fixed_price was derived from the list (rate card) price. Only meaningful when fixed_price is present.'
        ),
    ] = None
    eligible_adjustments: Annotated[
        list[adjustment_kind.PriceAdjustmentKind] | None,
        Field(
            description='Adjustment kinds applicable to this pricing option. Tells buyer agents which adjustments are available before negotiation. When absent, no adjustments are pre-declared — the buyer should check price_breakdown if present.'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var currency : str
var custom_event_name : str | None
var eligible_adjustments : list[adcp.types.generated_poc.enums.adjustment_kind.PriceAdjustmentKind] | None
var event_source_id : str | None
var event_type : adcp.types.generated_poc.enums.event_type.EventType
var fixed_price : float
var min_spend_per_package : float | None
var model_config
var price_breakdown : adcp.types.generated_poc.pricing_options.price_breakdown.PriceBreakdown | None
var pricing_model : Literal['cpa']
var pricing_option_id : str

Inherited members

class CpcPricingOption (**data: Any)
Expand source code
class CpcPricingOption(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    pricing_option_id: Annotated[
        str, Field(description='Unique identifier for this pricing option within the product')
    ]
    pricing_model: Annotated[Literal['cpc'], Field(description='Cost per click')] = 'cpc'
    currency: Annotated[
        str,
        Field(
            description='ISO 4217 currency code',
            examples=['USD', 'EUR', 'GBP', 'JPY'],
            pattern='^[A-Z]{3}$',
        ),
    ]
    fixed_price: Annotated[
        float | None,
        Field(
            description='Fixed price per click. If present, this is fixed pricing. If absent, auction-based.',
            ge=0.0,
        ),
    ] = None
    floor_price: Annotated[
        float | None,
        Field(
            description='Minimum acceptable bid for auction pricing (mutually exclusive with fixed_price). Bids below this value will be rejected.',
            ge=0.0,
        ),
    ] = None
    max_bid: Annotated[
        bool | None,
        Field(
            deprecated=True,
            description='DEPRECATED in 3.2 and removed in the next major. Legacy hint used only to normalize package bid_price to bidding.max_bid (true) or bidding.bid_amount (false/absent). New buyers express intent directly in bidding.',
        ),
    ] = False
    price_guidance: Annotated[
        price_guidance_1.PriceGuidance | None,
        Field(description='Optional pricing guidance for auction-based bidding'),
    ] = None
    min_spend_per_package: Annotated[
        float | None,
        Field(
            description='Minimum spend requirement per package using this pricing option, in the specified currency',
            ge=0.0,
        ),
    ] = None
    price_breakdown: Annotated[
        price_breakdown_1.PriceBreakdown | None,
        Field(
            description='Breakdown of how fixed_price was derived from the list (rate card) price. Only meaningful when fixed_price is present.'
        ),
    ] = None
    eligible_adjustments: Annotated[
        list[adjustment_kind.PriceAdjustmentKind] | None,
        Field(
            description='Adjustment kinds applicable to this pricing option. Tells buyer agents which adjustments are available before negotiation. When absent, no adjustments are pre-declared — the buyer should check price_breakdown if present.'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var currency : str
var eligible_adjustments : list[adcp.types.generated_poc.enums.adjustment_kind.PriceAdjustmentKind] | None
var fixed_price : float | None
var floor_price : float | None
var max_bid : bool | None
var min_spend_per_package : float | None
var model_config
var price_breakdown : adcp.types.generated_poc.pricing_options.price_breakdown.PriceBreakdown | None
var price_guidance : adcp.types.generated_poc.pricing_options.price_guidance.PriceGuidance | None
var pricing_model : Literal['cpc']
var pricing_option_id : str

Inherited members

class CpcvPricingOption (**data: Any)
Expand source code
class CpcvPricingOption(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    pricing_option_id: Annotated[
        str, Field(description='Unique identifier for this pricing option within the product')
    ]
    pricing_model: Annotated[
        Literal['cpcv'], Field(description='Cost per completed view (100% completion)')
    ] = 'cpcv'
    currency: Annotated[
        str,
        Field(
            description='ISO 4217 currency code',
            examples=['USD', 'EUR', 'GBP', 'JPY'],
            pattern='^[A-Z]{3}$',
        ),
    ]
    fixed_price: Annotated[
        float | None,
        Field(
            description='Fixed price per completed view. If present, this is fixed pricing. If absent, auction-based.',
            ge=0.0,
        ),
    ] = None
    floor_price: Annotated[
        float | None,
        Field(
            description='Minimum acceptable bid for auction pricing (mutually exclusive with fixed_price). Bids below this value will be rejected.',
            ge=0.0,
        ),
    ] = None
    max_bid: Annotated[
        bool | None,
        Field(
            deprecated=True,
            description='DEPRECATED in 3.2 and removed in the next major. Legacy hint used only to normalize package bid_price to bidding.max_bid (true) or bidding.bid_amount (false/absent). New buyers express intent directly in bidding.',
        ),
    ] = False
    price_guidance: Annotated[
        price_guidance_1.PriceGuidance | None,
        Field(description='Optional pricing guidance for auction-based bidding'),
    ] = None
    min_spend_per_package: Annotated[
        float | None,
        Field(
            description='Minimum spend requirement per package using this pricing option, in the specified currency',
            ge=0.0,
        ),
    ] = None
    price_breakdown: Annotated[
        price_breakdown_1.PriceBreakdown | None,
        Field(
            description='Breakdown of how fixed_price was derived from the list (rate card) price. Only meaningful when fixed_price is present.'
        ),
    ] = None
    eligible_adjustments: Annotated[
        list[adjustment_kind.PriceAdjustmentKind] | None,
        Field(
            description='Adjustment kinds applicable to this pricing option. Tells buyer agents which adjustments are available before negotiation. When absent, no adjustments are pre-declared — the buyer should check price_breakdown if present.'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var currency : str
var eligible_adjustments : list[adcp.types.generated_poc.enums.adjustment_kind.PriceAdjustmentKind] | None
var fixed_price : float | None
var floor_price : float | None
var max_bid : bool | None
var min_spend_per_package : float | None
var model_config
var price_breakdown : adcp.types.generated_poc.pricing_options.price_breakdown.PriceBreakdown | None
var price_guidance : adcp.types.generated_poc.pricing_options.price_guidance.PriceGuidance | None
var pricing_model : Literal['cpcv']
var pricing_option_id : str

Inherited members

class CpmPricingOption (**data: Any)
Expand source code
class CpmPricingOption(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    pricing_option_id: Annotated[
        str, Field(description='Unique identifier for this pricing option within the product')
    ]
    pricing_model: Annotated[Literal['cpm'], Field(description='Cost per 1,000 impressions')] = 'cpm'
    currency: Annotated[
        str,
        Field(
            description='ISO 4217 currency code',
            examples=['USD', 'EUR', 'GBP', 'JPY'],
            pattern='^[A-Z]{3}$',
        ),
    ]
    fixed_price: Annotated[
        float | None,
        Field(
            description='Fixed price per unit. If present, this is fixed pricing. If absent, auction-based.',
            ge=0.0,
        ),
    ] = None
    floor_price: Annotated[
        float | None,
        Field(
            description='Minimum acceptable bid for auction pricing (mutually exclusive with fixed_price). Bids below this value will be rejected.',
            ge=0.0,
        ),
    ] = None
    max_bid: Annotated[
        bool | None,
        Field(
            deprecated=True,
            description='DEPRECATED in 3.2 and removed in the next major. Legacy hint used only to normalize package bid_price to bidding.max_bid (true) or bidding.bid_amount (false/absent). New buyers express intent directly in bidding.',
        ),
    ] = False
    price_guidance: Annotated[
        price_guidance_1.PriceGuidance | None,
        Field(description='Optional pricing guidance for auction-based bidding'),
    ] = None
    min_spend_per_package: Annotated[
        float | None,
        Field(
            description='Minimum spend requirement per package using this pricing option, in the specified currency',
            ge=0.0,
        ),
    ] = None
    price_breakdown: Annotated[
        price_breakdown_1.PriceBreakdown | None,
        Field(
            description='Breakdown of how fixed_price was derived from the list (rate card) price. Only meaningful when fixed_price is present.'
        ),
    ] = None
    eligible_adjustments: Annotated[
        list[adjustment_kind.PriceAdjustmentKind] | None,
        Field(
            description='Adjustment kinds applicable to this pricing option. Tells buyer agents which adjustments are available before negotiation. When absent, no adjustments are pre-declared — the buyer should check price_breakdown if present.'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var currency : str
var eligible_adjustments : list[adcp.types.generated_poc.enums.adjustment_kind.PriceAdjustmentKind] | None
var fixed_price : float | None
var floor_price : float | None
var max_bid : bool | None
var min_spend_per_package : float | None
var model_config
var price_breakdown : adcp.types.generated_poc.pricing_options.price_breakdown.PriceBreakdown | None
var price_guidance : adcp.types.generated_poc.pricing_options.price_guidance.PriceGuidance | None
var pricing_model : Literal['cpm']
var pricing_option_id : str
class CpmAuctionPricingOption (**data: Any)
Expand source code
class CpmPricingOption(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    pricing_option_id: Annotated[
        str, Field(description='Unique identifier for this pricing option within the product')
    ]
    pricing_model: Annotated[Literal['cpm'], Field(description='Cost per 1,000 impressions')] = 'cpm'
    currency: Annotated[
        str,
        Field(
            description='ISO 4217 currency code',
            examples=['USD', 'EUR', 'GBP', 'JPY'],
            pattern='^[A-Z]{3}$',
        ),
    ]
    fixed_price: Annotated[
        float | None,
        Field(
            description='Fixed price per unit. If present, this is fixed pricing. If absent, auction-based.',
            ge=0.0,
        ),
    ] = None
    floor_price: Annotated[
        float | None,
        Field(
            description='Minimum acceptable bid for auction pricing (mutually exclusive with fixed_price). Bids below this value will be rejected.',
            ge=0.0,
        ),
    ] = None
    max_bid: Annotated[
        bool | None,
        Field(
            deprecated=True,
            description='DEPRECATED in 3.2 and removed in the next major. Legacy hint used only to normalize package bid_price to bidding.max_bid (true) or bidding.bid_amount (false/absent). New buyers express intent directly in bidding.',
        ),
    ] = False
    price_guidance: Annotated[
        price_guidance_1.PriceGuidance | None,
        Field(description='Optional pricing guidance for auction-based bidding'),
    ] = None
    min_spend_per_package: Annotated[
        float | None,
        Field(
            description='Minimum spend requirement per package using this pricing option, in the specified currency',
            ge=0.0,
        ),
    ] = None
    price_breakdown: Annotated[
        price_breakdown_1.PriceBreakdown | None,
        Field(
            description='Breakdown of how fixed_price was derived from the list (rate card) price. Only meaningful when fixed_price is present.'
        ),
    ] = None
    eligible_adjustments: Annotated[
        list[adjustment_kind.PriceAdjustmentKind] | None,
        Field(
            description='Adjustment kinds applicable to this pricing option. Tells buyer agents which adjustments are available before negotiation. When absent, no adjustments are pre-declared — the buyer should check price_breakdown if present.'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var currency : str
var eligible_adjustments : list[adcp.types.generated_poc.enums.adjustment_kind.PriceAdjustmentKind] | None
var fixed_price : float | None
var floor_price : float | None
var max_bid : bool | None
var min_spend_per_package : float | None
var model_config
var price_breakdown : adcp.types.generated_poc.pricing_options.price_breakdown.PriceBreakdown | None
var price_guidance : adcp.types.generated_poc.pricing_options.price_guidance.PriceGuidance | None
var pricing_model : Literal['cpm']
var pricing_option_id : str
class CpmFixedRatePricingOption (**data: Any)
Expand source code
class CpmPricingOption(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    pricing_option_id: Annotated[
        str, Field(description='Unique identifier for this pricing option within the product')
    ]
    pricing_model: Annotated[Literal['cpm'], Field(description='Cost per 1,000 impressions')] = 'cpm'
    currency: Annotated[
        str,
        Field(
            description='ISO 4217 currency code',
            examples=['USD', 'EUR', 'GBP', 'JPY'],
            pattern='^[A-Z]{3}$',
        ),
    ]
    fixed_price: Annotated[
        float | None,
        Field(
            description='Fixed price per unit. If present, this is fixed pricing. If absent, auction-based.',
            ge=0.0,
        ),
    ] = None
    floor_price: Annotated[
        float | None,
        Field(
            description='Minimum acceptable bid for auction pricing (mutually exclusive with fixed_price). Bids below this value will be rejected.',
            ge=0.0,
        ),
    ] = None
    max_bid: Annotated[
        bool | None,
        Field(
            deprecated=True,
            description='DEPRECATED in 3.2 and removed in the next major. Legacy hint used only to normalize package bid_price to bidding.max_bid (true) or bidding.bid_amount (false/absent). New buyers express intent directly in bidding.',
        ),
    ] = False
    price_guidance: Annotated[
        price_guidance_1.PriceGuidance | None,
        Field(description='Optional pricing guidance for auction-based bidding'),
    ] = None
    min_spend_per_package: Annotated[
        float | None,
        Field(
            description='Minimum spend requirement per package using this pricing option, in the specified currency',
            ge=0.0,
        ),
    ] = None
    price_breakdown: Annotated[
        price_breakdown_1.PriceBreakdown | None,
        Field(
            description='Breakdown of how fixed_price was derived from the list (rate card) price. Only meaningful when fixed_price is present.'
        ),
    ] = None
    eligible_adjustments: Annotated[
        list[adjustment_kind.PriceAdjustmentKind] | None,
        Field(
            description='Adjustment kinds applicable to this pricing option. Tells buyer agents which adjustments are available before negotiation. When absent, no adjustments are pre-declared — the buyer should check price_breakdown if present.'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var currency : str
var eligible_adjustments : list[adcp.types.generated_poc.enums.adjustment_kind.PriceAdjustmentKind] | None
var fixed_price : float | None
var floor_price : float | None
var max_bid : bool | None
var min_spend_per_package : float | None
var model_config
var price_breakdown : adcp.types.generated_poc.pricing_options.price_breakdown.PriceBreakdown | None
var price_guidance : adcp.types.generated_poc.pricing_options.price_guidance.PriceGuidance | None
var pricing_model : Literal['cpm']
var pricing_option_id : str

Inherited members

class CppPricingOption (**data: Any)
Expand source code
class CppPricingOption(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    pricing_option_id: Annotated[
        str, Field(description='Unique identifier for this pricing option within the product')
    ]
    pricing_model: Annotated[Literal['cpp'], Field(description='Cost per Gross Rating Point')] = 'cpp'
    currency: Annotated[
        str,
        Field(
            description='ISO 4217 currency code',
            examples=['USD', 'EUR', 'GBP', 'JPY'],
            pattern='^[A-Z]{3}$',
        ),
    ]
    fixed_price: Annotated[
        float | None,
        Field(
            description='Fixed price per rating point. If present, this is fixed pricing. If absent, auction-based.',
            ge=0.0,
        ),
    ] = None
    floor_price: Annotated[
        float | None,
        Field(
            description='Minimum acceptable bid for auction pricing (mutually exclusive with fixed_price). Bids below this value will be rejected.',
            ge=0.0,
        ),
    ] = None
    price_guidance: Annotated[
        price_guidance_1.PriceGuidance | None,
        Field(description='Optional pricing guidance for auction-based bidding'),
    ] = None
    parameters: Annotated[
        Parameters, Field(description='CPP-specific parameters for demographic targeting')
    ]
    min_spend_per_package: Annotated[
        float | None,
        Field(
            description='Minimum spend requirement per package using this pricing option, in the specified currency',
            ge=0.0,
        ),
    ] = None
    price_breakdown: Annotated[
        price_breakdown_1.PriceBreakdown | None,
        Field(
            description='Breakdown of how fixed_price was derived from the list (rate card) price. Only meaningful when fixed_price is present.'
        ),
    ] = None
    eligible_adjustments: Annotated[
        list[adjustment_kind.PriceAdjustmentKind] | None,
        Field(
            description='Adjustment kinds applicable to this pricing option. Tells buyer agents which adjustments are available before negotiation. When absent, no adjustments are pre-declared — the buyer should check price_breakdown if present.'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var currency : str
var eligible_adjustments : list[adcp.types.generated_poc.enums.adjustment_kind.PriceAdjustmentKind] | None
var fixed_price : float | None
var floor_price : float | None
var min_spend_per_package : float | None
var model_config
var parameters : adcp.types.generated_poc.pricing_options.cpp_option.Parameters
var price_breakdown : adcp.types.generated_poc.pricing_options.price_breakdown.PriceBreakdown | None
var price_guidance : adcp.types.generated_poc.pricing_options.price_guidance.PriceGuidance | None
var pricing_model : Literal['cpp']
var pricing_option_id : str

Inherited members

class CpvPricingOption (**data: Any)
Expand source code
class CpvPricingOption(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    pricing_option_id: Annotated[
        str, Field(description='Unique identifier for this pricing option within the product')
    ]
    pricing_model: Annotated[Literal['cpv'], Field(description='Cost per view at threshold')] = 'cpv'
    currency: Annotated[
        str,
        Field(
            description='ISO 4217 currency code',
            examples=['USD', 'EUR', 'GBP', 'JPY'],
            pattern='^[A-Z]{3}$',
        ),
    ]
    fixed_price: Annotated[
        float | None,
        Field(
            description='Fixed price per view. If present, this is fixed pricing. If absent, auction-based.',
            ge=0.0,
        ),
    ] = None
    floor_price: Annotated[
        float | None,
        Field(
            description='Minimum acceptable bid for auction pricing (mutually exclusive with fixed_price). Bids below this value will be rejected.',
            ge=0.0,
        ),
    ] = None
    max_bid: Annotated[
        bool | None,
        Field(
            deprecated=True,
            description='DEPRECATED in 3.2 and removed in the next major. Legacy hint used only to normalize package bid_price to bidding.max_bid (true) or bidding.bid_amount (false/absent). New buyers express intent directly in bidding.',
        ),
    ] = False
    price_guidance: Annotated[
        price_guidance_1.PriceGuidance | None,
        Field(description='Optional pricing guidance for auction-based bidding'),
    ] = None
    parameters: Annotated[
        Parameters, Field(description='CPV-specific parameters defining the view threshold')
    ]
    min_spend_per_package: Annotated[
        float | None,
        Field(
            description='Minimum spend requirement per package using this pricing option, in the specified currency',
            ge=0.0,
        ),
    ] = None
    price_breakdown: Annotated[
        price_breakdown_1.PriceBreakdown | None,
        Field(
            description='Breakdown of how fixed_price was derived from the list (rate card) price. Only meaningful when fixed_price is present.'
        ),
    ] = None
    eligible_adjustments: Annotated[
        list[adjustment_kind.PriceAdjustmentKind] | None,
        Field(
            description='Adjustment kinds applicable to this pricing option. Tells buyer agents which adjustments are available before negotiation. When absent, no adjustments are pre-declared — the buyer should check price_breakdown if present.'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var currency : str
var eligible_adjustments : list[adcp.types.generated_poc.enums.adjustment_kind.PriceAdjustmentKind] | None
var fixed_price : float | None
var floor_price : float | None
var max_bid : bool | None
var min_spend_per_package : float | None
var model_config
var parameters : adcp.types.generated_poc.pricing_options.cpv_option.Parameters
var price_breakdown : adcp.types.generated_poc.pricing_options.price_breakdown.PriceBreakdown | None
var price_guidance : adcp.types.generated_poc.pricing_options.price_guidance.PriceGuidance | None
var pricing_model : Literal['cpv']
var pricing_option_id : str

Inherited members

class CreateContentStandardsSuccessResponse (**data: Any)
Expand source code
class CreateContentStandardsResponse(AdcpVersionEnvelope, ProtocolEnvelope):
    pass

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • adcp.types.generated_poc.core.protocol_envelope.ProtocolEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var model_config
class CreateContentStandardsResponse1 (**data: Any)
Expand source code
class CreateContentStandardsResponse(AdcpVersionEnvelope, ProtocolEnvelope):
    pass

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • adcp.types.generated_poc.core.protocol_envelope.ProtocolEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var model_config
class CreateContentStandardsErrorResponse (**data: Any)
Expand source code
class CreateContentStandardsResponse(AdcpVersionEnvelope, ProtocolEnvelope):
    pass

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • adcp.types.generated_poc.core.protocol_envelope.ProtocolEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var model_config

Inherited members

class CreateMediaBuyRequest (**data: Any)

Base class enforcing the primary canonical runtime boundary.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var account : adcp.types.generated_poc.core.account_ref.AccountReference
var adcp_major_version : int | None
var adcp_version : str | None
var advertiser_industry : adcp.types.generated_poc.enums.advertiser_industry.AdvertiserIndustry | None
var agency_estimate_number : str | None
var artifact_webhook : adcp.types.generated_poc.media_buy.create_media_buy_request.ArtifactWebhook | None
var bidding : adcp.types.generated_poc.core.bidding_policy.BiddingPolicy | None
var brand : adcp.types.generated_poc.core.brand_ref.BrandReference
var budget_allocation : adcp.types.generated_poc.core.budget_allocation.BudgetAllocation | None
var budget_cap_timezone : str | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var daily_budget_cap : float | None
var end_time : pydantic.types.AwareDatetime
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var governance_context : str | None
var idempotency_key : str
var invoice_recipient : adcp.types.generated_poc.core.business_entity.BusinessEntity | None
var io_acceptance : adcp.types.generated_poc.media_buy.create_media_buy_request.IoAcceptance | None
var model_config
var name : str | None
var opportunity : adcp.types.generated_poc.media_buy.create_media_buy_request.Opportunity | None
var pacing : adcp.types.generated_poc.enums.pacing.Pacing | None
var packages : list[PackageRequest] | None
var paused : bool | None
var plan_id : str | None
var po_number : str | None
var proposal_id : str | None
var push_notification_config : adcp.types.generated_poc.core.push_notification_config.PushNotificationConfig | None
var reporting_webhook : adcp.types.generated_poc.core.reporting_webhook.ReportingWebhook | None
var start_time : adcp.types.generated_poc.core.start_timing.StartTiming
var total_budget : adcp.types.generated_poc.media_buy.create_media_buy_request.TotalBudget | None
class LegacyCreateMediaBuyRequest (**data: Any)
Expand source code
class CreateMediaBuyRequest(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    governance_context: Annotated[
        str | None,
        Field(
            description='Opaque intent authorization for this media-buy commitment. Required when governance applies to the resolved account.',
            max_length=4096,
            min_length=1,
            pattern='^[\\x20-\\x7E]+$',
        ),
    ] = None
    idempotency_key: Annotated[
        str,
        Field(
            description='Client-generated unique key for this request. If a request with the same idempotency_key and account has already been processed, the seller returns the existing media buy rather than creating a duplicate. MUST be unique per (seller, request) pair to prevent cross-seller correlation. Use a fresh UUID v4 for each request.',
            max_length=255,
            min_length=16,
            pattern='^[A-Za-z0-9_.:-]{16,255}$',
        ),
    ]
    plan_id: Annotated[
        str | None,
        Field(
            deprecated=True,
            description='DEPRECATED on seller-facing requests. New buyers send the approved governance_context on the protocol envelope; the seller forwards that opaque context and does not need the plan identifier. If both are present, the governance agent MUST reject a mismatch. Removed in 4.0.',
        ),
    ] = None
    account: Annotated[
        account_ref.AccountReference,
        Field(
            description='Account to bill for this media buy. Pass a natural key (brand, operator, optional sandbox) or a seller-assigned account_id from list_accounts.'
        ),
    ]
    proposal_id: Annotated[
        str | None,
        Field(
            description="ID of the exact committed proposal snapshot to execute. With total_budget, the publisher creates packages using the proposal's fixed percentages or seller-optimized constraints. Mutually exclusive: provide packages or proposal_id, not both. AdCP 3.2 request_proposals and ordinary refine_proposals revisions issue drafts; refine_proposals action finalize creates the executable committed hold. Sellers reject draft, declined, or previously executed snapshots, while exact retries with the original idempotency key replay historical success. Changed commercial terms are issued under a new proposal_id, so no separate proposal version is required."
        ),
    ] = None
    opportunity: Annotated[
        Opportunity | None,
        Field(
            description='Optional planning-cycle closure. Sellers infer successful proposal execution as closed with close_reason accepted_with_seller when status is omitted; when status is present it MUST be closed with that reason. If the proposal was issued under an opportunity_id, a supplied ID MUST match it.'
        ),
    ] = None
    total_budget: Annotated[
        TotalBudget | None,
        Field(
            description='Hard aggregate lifetime budget for the media buy. Required when executing a proposal and for seller-optimized explicit packages. Optional in fixed explicit-package mode; when present there, amount MUST equal the sum of package budgets. For a fixed proposal, the publisher applies allocation percentages to this amount. For a seller-optimized proposal or explicit buy, packages draw dynamically from this shared total.'
        ),
    ] = None
    daily_budget_cap: Annotated[
        float | None,
        Field(
            description='Optional hard aggregate daily spend ceiling in the media-buy currency. It limits total spend without allocating package amounts. Package caps are subordinate and need not sum to it. Requires advertised media_buy budget-capping scope; otherwise rejected with UNSUPPORTED_FEATURE.',
            ge=0.0,
        ),
    ] = None
    budget_cap_timezone: Annotated[
        str | None,
        Field(
            description='Optional shared IANA day boundary override for all caps. Requires buyer_timezone_override; otherwise rejected with UNSUPPORTED_FEATURE. When omitted, budget_capping.timezone_basis selects Account.timezone or the advertised fixed_timezone.',
            min_length=1,
        ),
    ] = None
    budget_allocation: Annotated[
        budget_allocation_1.BudgetAllocation | None,
        Field(
            description='How budget is allocated across explicit packages. Omit for legacy fixed allocation. In proposal mode the committed proposal supplies this configuration and callers MUST omit it here.'
        ),
    ] = None
    packages: Annotated[
        Sequence[package_request.PackageRequest] | None,
        Field(
            description='Array of package configurations. Required when not using proposal_id. Mutually exclusive: provide packages or proposal_id, not both. Fixed allocation requires budget on every package. Seller-optimized allocation permits package budget to be omitted or to act as a hard cap. When executing a proposal, omit packages; the seller derives them from the committed proposal.',
            min_length=1,
        ),
    ] = None
    brand: Annotated[
        brand_ref.BrandReference,
        Field(
            description='Brand reference for this media buy. Resolved to full brand identity at execution time from brand.json or the registry.'
        ),
    ]
    advertiser_industry: Annotated[
        advertiser_industry_1.AdvertiserIndustry | None,
        Field(
            description="Industry classification for this specific campaign. A brand may operate across multiple industries (brand.json industries field), but each media buy targets one. For example, a consumer health company running a wellness campaign sends 'healthcare.wellness', not 'cpg'. Sellers map this to platform-native codes (e.g., Spotify ADV categories, LinkedIn industry IDs). When omitted, sellers may infer from the brand manifest's industries field."
        ),
    ] = None
    invoice_recipient: Annotated[
        business_entity.BusinessEntity | None,
        Field(
            description="Override the account's default billing entity for this specific buy. When provided, the seller invoices this entity instead. The seller MUST validate the invoice recipient is authorized for this account. When governance_agents are configured, the seller MUST include invoice_recipient in the check_governance request."
        ),
    ] = None
    io_acceptance: Annotated[
        IoAcceptance | None,
        Field(
            description="Acceptance of an insertion order from a committed proposal. Required when the proposal's insertion_order has requires_signature: true. References the io_id from the proposal's insertion_order."
        ),
    ] = None
    po_number: Annotated[str | None, Field(description='Purchase order number for tracking')] = None
    name: Annotated[
        str | None,
        Field(
            description='Human-readable name for this media buy, shared by buyer and seller for trafficking UI display and operational communication. When supplied, the seller MUST persist it and echo it unchanged on the create success response and subsequent get_media_buys reads. This display label is not an identifier or financial reference.',
            max_length=255,
            min_length=1,
            pattern='\\S',
        ),
    ] = None
    agency_estimate_number: Annotated[
        str | None,
        Field(
            description="Agency estimate or authorization number. Primary financial reference for broadcast buys — links the order to the agency's media plan and billing system. Travels with the order and creative traffic identifiers through the transaction lifecycle.",
            max_length=100,
        ),
    ] = None
    start_time: start_timing.StartTiming
    end_time: Annotated[
        AwareDatetime, Field(description='Campaign end date/time in ISO 8601 format')
    ]
    pacing: Annotated[
        pacing_1.Pacing | None,
        Field(
            description='Aggregate pacing strategy for the media-buy budget across the media-buy flight. This controls how much the buy spends over time. Package pacing is subordinate and influences which package receives the aggregate spend; package pacing MUST NOT cause aggregate delivery to exceed this strategy. Defaults to even when total_budget is present. When executing a proposal that carries pacing, omit this field or send the identical value; the seller MUST reject a conflicting override with TERMS_REJECTED.'
        ),
    ] = None
    bidding: Annotated[
        bidding_policy.BiddingPolicy | None,
        Field(
            description="Complete media-buy bidding default inherited by packages that omit package.bidding. `{automatic:true}` records an explicit automatic policy. In seller-optimized mode, cost_per/roas bind to the primary budget_allocation_1.optimization_goals goal. In fixed mode, inherited cost_per is valid only when inheriting package primary-goal result units are compatible; inherited roas requires value-bearing primary goals. Every monetary field uses total_budget.currency or the single currency derived for the media buy, and every affected pricing option MUST declare that currency. Sellers MUST reject incompatible units, combinations, currency, or overrides before mutation with BIDDING_PLACEMENT_CONFLICT. Media-buy bidding combined with any inheriting package's legacy bid_price or legacy monetary optimization-goal target is ambiguous and MUST be rejected with AMBIGUOUS_BIDDING_POLICY."
        ),
    ] = None
    paused: Annotated[
        bool | None,
        Field(
            description="Create the media buy in a paused delivery state. When true, and the buy would otherwise be active because creatives are assigned and the flight has started, the seller returns media_buy_status 'paused'. Setup blockers still take precedence: a buy with no creatives remains 'pending_creatives', and a future-dated buy remains 'pending_start' until its flight can start. Defaults to false."
        ),
    ] = False
    push_notification_config: Annotated[
        push_notification_config_1.PushNotificationConfig | None,
        Field(
            description='Optional webhook configuration for async task status notifications. Publisher will send webhooks when status changes (working, input-required, completed, failed, canceled). Buyers SHOULD supply `push_notification_config_1.operation_id` as the canonical correlation value; publishers echo that field back verbatim in webhook payloads and MUST NOT parse the URL to derive it.'
        ),
    ] = None
    reporting_webhook: Annotated[
        reporting_webhook_1.ReportingWebhook | None,
        Field(description='Optional webhook configuration for automated reporting delivery'),
    ] = None
    artifact_webhook: Annotated[
        ArtifactWebhook | None,
        Field(
            description='Optional webhook configuration for content artifact delivery. Used by governance agents to validate content adjacency. Seller pushes artifacts to this endpoint; orchestrator forwards to governance agent for validation.'
        ),
    ] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var account : adcp.types.generated_poc.core.account_ref.AccountReference
var advertiser_industry : adcp.types.generated_poc.enums.advertiser_industry.AdvertiserIndustry | None
var agency_estimate_number : str | None
var artifact_webhook : adcp.types.generated_poc.media_buy.create_media_buy_request.ArtifactWebhook | None
var bidding : adcp.types.generated_poc.core.bidding_policy.BiddingPolicy | None
var brand : adcp.types.generated_poc.core.brand_ref.BrandReference
var budget_allocation : adcp.types.generated_poc.core.budget_allocation.BudgetAllocation | None
var budget_cap_timezone : str | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var daily_budget_cap : float | None
var end_time : pydantic.types.AwareDatetime
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var governance_context : str | None
var idempotency_key : str
var invoice_recipient : adcp.types.generated_poc.core.business_entity.BusinessEntity | None
var io_acceptance : adcp.types.generated_poc.media_buy.create_media_buy_request.IoAcceptance | None
var model_config
var name : str | None
var opportunity : adcp.types.generated_poc.media_buy.create_media_buy_request.Opportunity | None
var pacing : adcp.types.generated_poc.enums.pacing.Pacing | None
var packages : collections.abc.Sequence[adcp.types.generated_poc.media_buy.package_request.PackageRequest] | None
var paused : bool | None
var po_number : str | None
var proposal_id : str | None
var push_notification_config : adcp.types.generated_poc.core.push_notification_config.PushNotificationConfig | None
var reporting_webhook : adcp.types.generated_poc.core.reporting_webhook.ReportingWebhook | None
var start_time : adcp.types.generated_poc.core.start_timing.StartTiming
var total_budget : adcp.types.generated_poc.media_buy.create_media_buy_request.TotalBudget | None

Instance variables

var adcp_major_version : int | None
Expand source code
def __get__(self, obj: BaseModel | None, obj_type: type[BaseModel] | None = None) -> Any:
    if obj is None:
        if self.wrapped_property is not None:
            return self.wrapped_property.__get__(None, obj_type)
        raise AttributeError(self.field_name)

    warnings.warn(self.msg, DeprecationWarning, stacklevel=2)

    if self.wrapped_property is not None:
        return self.wrapped_property.__get__(obj, obj_type)
    return obj.__dict__[self.field_name]

Read-only data descriptor used to emit a runtime deprecation warning before accessing a deprecated field.

Attributes
-----=
msg
The deprecation message to be emitted.
wrapped_property
The property instance if the deprecated field is a computed field, or None.
field_name
The name of the field being deprecated.
var plan_id : str | None
Expand source code
def __get__(self, obj: BaseModel | None, obj_type: type[BaseModel] | None = None) -> Any:
    if obj is None:
        if self.wrapped_property is not None:
            return self.wrapped_property.__get__(None, obj_type)
        raise AttributeError(self.field_name)

    warnings.warn(self.msg, DeprecationWarning, stacklevel=2)

    if self.wrapped_property is not None:
        return self.wrapped_property.__get__(obj, obj_type)
    return obj.__dict__[self.field_name]

Read-only data descriptor used to emit a runtime deprecation warning before accessing a deprecated field.

Attributes
-----=
msg
The deprecation message to be emitted.
wrapped_property
The property instance if the deprecated field is a computed field, or None.
field_name
The name of the field being deprecated.

Inherited members

class CreateMediaBuySuccessResponse (**data: Any)
Expand source code
class CreateMediaBuyResponse1(_CreateMediaBuyResponse1Base):
    """Canonical create response preserving the 3.x legacy-status normalizer."""

    @model_validator(mode="before")
    @classmethod
    def _normalize_legacy_status(cls, data: Any) -> Any:
        if not isinstance(data, dict):
            return data
        raw_status = unwrap_enum_value(data.get("status"))
        media_buy_status = unwrap_enum_value(data.get("media_buy_status"))
        if raw_status is None or raw_status == "completed":
            return {**data, "status": "completed"}
        if media_buy_status is None and raw_status in MEDIA_BUY_LEGACY_STATUS_VALUES:
            return {**data, "media_buy_status": raw_status, "status": "completed"}
        if media_buy_status is not None and raw_status == media_buy_status:
            return {**data, "status": "completed"}
        return data

Canonical create response preserving the 3.x legacy-status normalizer.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var model_config
class CreateMediaBuyResponse1 (**data: Any)
Expand source code
class CreateMediaBuyResponse1(_CreateMediaBuyResponse1Base):
    """Canonical create response preserving the 3.x legacy-status normalizer."""

    @model_validator(mode="before")
    @classmethod
    def _normalize_legacy_status(cls, data: Any) -> Any:
        if not isinstance(data, dict):
            return data
        raw_status = unwrap_enum_value(data.get("status"))
        media_buy_status = unwrap_enum_value(data.get("media_buy_status"))
        if raw_status is None or raw_status == "completed":
            return {**data, "status": "completed"}
        if media_buy_status is None and raw_status in MEDIA_BUY_LEGACY_STATUS_VALUES:
            return {**data, "media_buy_status": raw_status, "status": "completed"}
        if media_buy_status is not None and raw_status == media_buy_status:
            return {**data, "status": "completed"}
        return data

Canonical create response preserving the 3.x legacy-status normalizer.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var model_config

Inherited members

class CreateMediaBuyErrorResponse (**data: Any)

Base class enforcing the primary canonical runtime boundary.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var adcp_major_version : int | None
var adcp_version : str | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var errors : list[adcp.types.generated_poc.core.error.Error]
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config

Inherited members

class CreateMediaBuySubmittedResponse (**data: Any)

Base class enforcing the primary canonical runtime boundary.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var adcp_error : adcp.types.generated_poc.core.error.Error | None
var adcp_major_version : int | None
var adcp_version : str | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var context_id : str | None
var errors : list[adcp.types.generated_poc.core.error.Error] | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var governance_context : str | None
var message : str | None
var model_config
var payload : dict[str, typing.Any] | None
var push_notification_config : adcp.types.generated_poc.core.push_notification_config.PushNotificationConfig | None
var replayed : bool | None
var status : Literal[<TaskStatus.submitted: 'submitted'>]
var task_id : str
var timestamp : pydantic.types.AwareDatetime | None

Inherited members

class Creative (**data: Any)

Base class enforcing the primary canonical runtime boundary.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var account : adcp.types.generated_poc.core.account.Account | None
var assets : dict[str, adcp.types.generated_poc.core.assets.asset_union.AssetVariant | adcp.types.generated_poc.creative.list_creatives_response.Assets] | None
var assignments : adcp.types.generated_poc.creative.list_creatives_response.Assignments1 | None
var concept_id : str | None
var concept_name : str | None
var created_date : pydantic.types.AwareDatetime
var creative_id : str
var format_kind : adcp.types.generated_poc.core.canonical_format_kind.CanonicalFormatKind
var format_option_ref : adcp.types.generated_poc.core.format_option_ref.FormatOptionReference | None
var items : list[adcp.types.generated_poc.core.creative_item.CreativeItem] | None
var localization : adcp.types.generated_poc.core.creative_localization_readback.CreativeLocalizationReadback | None
var localization_unavailable : adcp.types.generated_poc.creative.list_creatives_response.LocalizationUnavailable | None
var model_config
var name : str
var pricing_options : list[adcp.types.generated_poc.core.vendor_pricing_option.VendorPricingOption] | None
var purge : adcp.types.generated_poc.creative.list_creatives_response.Purge | None
var rights : list[adcp.types.generated_poc.core.rights_constraint.RightsConstraint] | None
var rights_attestation_evaluations : list[adcp.types.generated_poc.core.rights_attestation_evaluation.RightsAttestationEvaluation] | None
var snapshot : adcp.types.generated_poc.creative.list_creatives_response.Snapshot | None
var snapshot_unavailable_reason : adcp.types.generated_poc.enums.snapshot_unavailable_reason.SnapshotUnavailableReason | None
var status : adcp.types.generated_poc.enums.creative_status.CreativeStatus
var tags : list[str] | None
var updated_date : pydantic.types.AwareDatetime
var variables : list[adcp.types.generated_poc.core.creative_variable.CreativeVariable] | None
var webhook_activity : list[adcp.types.generated_poc.core.webhook_activity_record.WebhookActivityRecord] | None
class SyncCreativeResult (**data: Any)
Expand source code
class Creative(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    creative_id: str
    account: account_1.Account | None = None
    action: creative_action_1.CreativeAction
    status: creative_status_1.CreativeStatus | None = None
    platform_id: str | None = None
    localization: creative_localization_readback_1.CreativeLocalizationReadback | None = None
    changes: list[str] | None = None
    errors: list[error_1.Error] | None = None
    warnings: list[str] | None = None
    preview_url: AnyUrl | None = None
    expires_at: AwareDatetime | None = None
    assigned_to: list[str] | None = None
    assignment_errors: dict[Annotated[str, StringConstraints(pattern='^[a-zA-Z0-9_-]+$')], str] | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var account : adcp.types.generated_poc.core.account.Account | None
var action : adcp.types.generated_poc.enums.creative_action.CreativeAction
var assigned_to : list[str] | None
var assignment_errors : dict[str, str] | None
var changes : list[str] | None
var creative_id : str
var errors : list[adcp.types.generated_poc.core.error.Error] | None
var expires_at : pydantic.types.AwareDatetime | None
var localization : adcp.types.generated_poc.core.creative_localization_readback.CreativeLocalizationReadback | None
var model_config
var platform_id : str | None
var preview_url : pydantic.networks.AnyUrl | None
var status : adcp.types.generated_poc.enums.creative_status.CreativeStatus | None
var warnings : list[str] | None

Inherited members

class CreativeApproval (**data: Any)
Expand source code
class CreativeApproval(IndicatorBearingResourceState):
    model_config = ConfigDict(
        extra='allow',
    )
    indicator_types_evaluated: Annotated[
        list[IndicatorTypesEvaluatedEnum2] | None,
        Field(
            description='Indicator types covered by this snapshot. Required whenever indicators is present. Types omitted from this list remain unknown even when indicators is empty. Every returned indicator.type MUST appear in this list.',
            min_length=1,
        ),
    ] = None
    indicators: Annotated[
        list[Indicator2] | None,
        Field(
            description='Current seller assertions for the indicator types and publisher/placement coverage named by the sibling evaluation fields. Omitted means unknown or not evaluated. A present empty array means evaluated with no current assertion for indicator_types_evaluated in the evaluated scope.'
        ),
    ] = None
    creative_id: Annotated[str, Field(description='Creative identifier')]
    approval_status: creative_approval_status.CreativeApprovalStatus
    rejection_reason: Annotated[
        str | None,
        Field(
            description="Human-readable explanation of why the creative was rejected. Present only when approval_status is 'rejected'."
        ),
    ] = None
    approval_scopes: Annotated[
        list[creative_approval_scope.ScopedCreativeApproval] | None,
        Field(
            description='Complete, disjoint publisher/placement approval partition when approval_status is partially_approved. A normalized scope appears once. For one publisher, use either one publisher-wide row or placement-specific rows, never both. Omit when one approval_status applies uniformly to the whole assignment. The same scoped outcomes are mirrored on list_creatives.',
            min_length=2,
        ),
    ] = None
    indicators_as_of: Annotated[
        AwareDatetime | None,
        Field(
            description='When the seller last completed the evaluation represented by indicators for this relationship. Required whenever indicators is present, including an empty array.'
        ),
    ] = None
    indicators_evaluated_scope: Annotated[
        list[indicator_scope.IndicatorScope] | None,
        Field(
            description='Optional publisher or placement scopes covered by this evaluation. Omit when indicators covers the whole package–creative assignment. When present, scopes not listed remain unknown; every returned indicator.scope entry MUST be contained by this set.',
            min_length=1,
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.indicator_bearing.IndicatorBearingResourceState
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var approval_scopes : list[adcp.types.generated_poc.core.creative_approval_scope.ScopedCreativeApproval] | None
var approval_status : adcp.types.generated_poc.enums.creative_approval_status.CreativeApprovalStatus
var creative_id : str
var indicator_types_evaluated : list[adcp.types.generated_poc.media_buy.get_media_buys_response.IndicatorTypesEvaluatedEnum2] | None
var indicators : list[adcp.types.generated_poc.media_buy.get_media_buys_response.Indicator2] | None
var indicators_as_of : pydantic.types.AwareDatetime | None
var indicators_evaluated_scope : list[adcp.types.generated_poc.core.indicator_scope.IndicatorScope] | None
var model_config
var rejection_reason : str | None

Inherited members

class CreativeApprovalStatus (*args, **kwds)
Expand source code
class CreativeApprovalStatus(StrEnum):
    pending_review = 'pending_review'
    approved = 'approved'
    partially_approved = 'partially_approved'
    rejected = 'rejected'

Enum where members are also (and must be) strings

Ancestors

  • enum.StrEnum
  • builtins.str
  • enum.ReprEnum
  • enum.Enum

Class variables

var approved
var partially_approved
var pending_review
var rejected
class LegacyCreativeAsset (root: RootModelRootType = PydanticUndefined, **data)
Expand source code
class CreativeAsset(RootModel[CreativeAsset1 | CreativeAsset2]):
    root: Annotated[
        CreativeAsset1 | CreativeAsset2,
        Field(
            description='Creative asset for upload to library — supports static assets, generative formats, and third-party snippets. Identifies which format this creative conforms to via EITHER a legacy `format_id` (structured `{agent_url, id}`) OR a 3.1+ `format_kind` (canonical format name), with optional `format_option_ref` when the target product needs disambiguation. Mutually exclusive — see the `oneOf` at the schema root.',
            title='Creative Asset',
        ),
    ]
    def __getattr__(self, name: str) -> Any:
        """Proxy attribute access to the wrapped type."""
        if name.startswith('_'):
            raise AttributeError(name)
        return getattr(self.root, name)

Usage Documentation

RootModel and Custom Root Types

A Pydantic BaseModel for the root object of the model.

Attributes
-----=
root
The root object of the model.
__pydantic_root_model__
Whether the model is a RootModel.
__pydantic_private__
Private fields in the model.
__pydantic_extra__
Extra fields in the model.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • pydantic.root_model.RootModel[Union[CreativeAsset1, CreativeAsset2]]
  • pydantic.root_model.RootModel
  • pydantic.main.BaseModel
  • typing.Generic

Class variables

var model_config
var root : adcp.types.generated_poc.core.creative_asset.CreativeAsset1 | adcp.types.generated_poc.core.creative_asset.CreativeAsset2
class LegacyCreativeFilters (**data: Any)
Expand source code
class CreativeFilters(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    accounts: Annotated[
        list[account_ref.AccountReference] | None,
        Field(
            description='Filter creatives by owning accounts. Useful for agencies managing multiple client accounts.',
            min_length=1,
        ),
    ] = None
    statuses: Annotated[
        list[creative_status.CreativeStatus] | None,
        Field(description='Filter by creative approval statuses', min_length=1),
    ] = None
    tags: Annotated[
        list[str] | None,
        Field(description='Filter by creative tags (all tags must match)', min_length=1),
    ] = None
    tags_any: Annotated[
        list[str] | None,
        Field(description='Filter by creative tags (any tag must match)', min_length=1),
    ] = None
    name_contains: Annotated[
        str | None,
        Field(description='Filter by creative names containing this text (case-insensitive)'),
    ] = None
    creative_ids: Annotated[
        list[str] | None,
        Field(description='Filter by specific creative IDs', max_length=100, min_length=1),
    ] = None
    created_after: Annotated[
        AwareDatetime | None,
        Field(description='Filter creatives created after this date (ISO 8601)'),
    ] = None
    created_before: Annotated[
        AwareDatetime | None,
        Field(description='Filter creatives created before this date (ISO 8601)'),
    ] = None
    updated_after: Annotated[
        AwareDatetime | None,
        Field(description='Filter creatives last updated after this date (ISO 8601)'),
    ] = None
    updated_before: Annotated[
        AwareDatetime | None,
        Field(description='Filter creatives last updated before this date (ISO 8601)'),
    ] = None
    assigned_to_packages: Annotated[
        list[str] | None,
        Field(
            description='Filter creatives assigned to any of these packages. Sales-agent-specific — standalone creative agents SHOULD ignore this filter.',
            min_length=1,
        ),
    ] = None
    media_buy_ids: Annotated[
        list[str] | None,
        Field(
            description='Filter creatives assigned to any of these media buys. Sales-agent-specific — standalone creative agents SHOULD ignore this filter.',
            min_length=1,
        ),
    ] = None
    unassigned: Annotated[
        bool | None,
        Field(
            description='Filter for unassigned creatives when true, assigned creatives when false. Sales-agent-specific — standalone creative agents SHOULD ignore this filter.'
        ),
    ] = None
    has_served: Annotated[
        bool | None,
        Field(
            description='When true, return only creatives that have served at least one impression. When false, return only creatives that have never served.'
        ),
    ] = None
    indicator_types: Annotated[
        list[indicator_type.IndicatorType] | None,
        Field(
            description='Return creatives with at least one package assignment carrying any requested current indicator type. Values within this field use OR logic; this field composes with other filters using AND logic. Sales-agent-specific: sellers support this filter only when media_buy.relationship_notifications.projection_tasks includes list_creatives. Other agents SHOULD ignore it and apply remaining filters. Buyers needing exact results MUST verify capability support and paginate the outer result set; assignment_projection: matching bounds nested rows.',
            min_length=1,
        ),
    ] = None
    concept_ids: Annotated[
        list[str] | None,
        Field(
            description='Filter by creative concept IDs. Concepts group related creatives across sizes and formats (e.g., Flashtalking concepts, Celtra campaign folders, CM360 creative groups).',
            min_length=1,
        ),
    ] = None
    format_ids: Annotated[
        list[format_id.FormatReferenceStructuredObject] | None,
        Field(
            deprecated=True,
            description='Deprecated in AdCP 3.2; removed in AdCP 4.0. Filter legacy named-format creatives. Use `format_kinds` for canonical libraries.',
            min_length=1,
        ),
    ] = None
    format_kinds: Annotated[
        list[canonical_format_kind.CanonicalFormatKind] | None,
        Field(
            description='Filter by canonical format kinds. Returns creatives matching any supplied kind.',
            min_length=1,
        ),
    ] = None
    asset_types: Annotated[
        list[asset_content_type.AssetContentType] | None,
        Field(
            description="Filter by asset types present on direct object values in the creative's top-level `assets` map. A creative matches when any directly assigned object has an `asset_type` in this array (OR within this field); this filter is conjunctive with every other active filter (AND across fields). Do not inspect array-valued slots or recurse into nested asset fields such as `cards[].media`; broader traversal is deferred. Agents that do not implement this filter MUST ignore it and apply the remaining filters rather than reject the request. Exact asset-type values use the shared AssetContentType vocabulary; `published_post` selects existing-published-post reference creatives without relying on publisher-specific format IDs.",
            min_length=1,
        ),
    ] = None
    has_variables: Annotated[
        bool | None,
        Field(
            description='When true, return only creatives with dynamic variables (DCO). When false, return only static creatives.'
        ),
    ] = None
    ext: Annotated[
        ext_1.ExtensionObject | None,
        Field(
            description='Vendor-namespaced extension parameters for seller- or platform-specific creative filter criteria not covered by standard fields. Keys MUST be namespaced under a vendor or platform key (e.g., ext.gam, ext.platform_x). Sellers MUST treat all values as untrusted buyer input; avoid unbounded logging or labels, and do not interpolate values into caller-visible error strings, LLM prompts, SQL queries, or system commands without sanitization. Persistent use of an extension key across multiple buyers is a signal to propose standardization.'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var accounts : list[adcp.types.generated_poc.core.account_ref.AccountReference] | None
var asset_types : list[adcp.types.generated_poc.enums.asset_content_type.AssetContentType] | None
var assigned_to_packages : list[str] | None
var concept_ids : list[str] | None
var created_after : pydantic.types.AwareDatetime | None
var created_before : pydantic.types.AwareDatetime | None
var creative_ids : list[str] | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var format_ids : list[adcp.types.generated_poc.core.format_id.FormatReferenceStructuredObject] | None
var format_kinds : list[adcp.types.generated_poc.core.canonical_format_kind.CanonicalFormatKind] | None
var has_served : bool | None
var has_variables : bool | None
var indicator_types : list[adcp.types.generated_poc.enums.indicator_type.IndicatorType] | None
var media_buy_ids : list[str] | None
var model_config
var name_contains : str | None
var statuses : list[adcp.types.generated_poc.enums.creative_status.CreativeStatus] | None
var tags : list[str] | None
var tags_any : list[str] | None
var unassigned : bool | None
var updated_after : pydantic.types.AwareDatetime | None
var updated_before : pydantic.types.AwareDatetime | None
class CreativeFilters (**data: Any)

Base class enforcing the primary canonical runtime boundary.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var accounts : list[adcp.types.generated_poc.core.account_ref.AccountReference] | None
var asset_types : list[adcp.types.generated_poc.enums.asset_content_type.AssetContentType] | None
var assigned_to_packages : list[str] | None
var concept_ids : list[str] | None
var created_after : pydantic.types.AwareDatetime | None
var created_before : pydantic.types.AwareDatetime | None
var creative_ids : list[str] | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var format_kinds : list[adcp.types.generated_poc.core.canonical_format_kind.CanonicalFormatKind] | None
var has_served : bool | None
var has_variables : bool | None
var indicator_types : list[adcp.types.generated_poc.enums.indicator_type.IndicatorType] | None
var media_buy_ids : list[str] | None
var model_config
var name_contains : str | None
var statuses : list[adcp.types.generated_poc.enums.creative_status.CreativeStatus] | None
var tags : list[str] | None
var tags_any : list[str] | None
var unassigned : bool | None
var updated_after : pydantic.types.AwareDatetime | None
var updated_before : pydantic.types.AwareDatetime | None

Inherited members

class CreativeManifest (**data: Any)
Expand source code
class CreativeManifest(_CreativeManifestBase):
    """Canonical manifest accepting the SDK's public standalone asset models.

    The 3.2 aggregate asset-union schema currently generates structurally
    duplicate Pydantic classes. Convert public ``ImageContent``/``UrlContent``
    (and peers) back to their wire dictionaries before the aggregate union
    validates them. This keeps the public constructors composable without
    relaxing the on-wire discriminator checks.
    """

    @model_validator(mode="before")
    @classmethod
    def _normalize_standalone_assets(cls, data: Any) -> Any:
        if not isinstance(data, dict) or not isinstance(data.get("assets"), dict):
            return data

        def wire_value(value: Any) -> Any:
            if isinstance(value, AdCPBaseModel):
                return value.model_dump(mode="json", exclude_none=True)
            if isinstance(value, list):
                return [wire_value(item) for item in value]
            return value

        return {
            **data,
            "assets": {key: wire_value(value) for key, value in data["assets"].items()},
        }

Canonical manifest accepting the SDK's public standalone asset models.

The 3.2 aggregate asset-union schema currently generates structurally duplicate Pydantic classes. Convert public ImageAsset/UrlAsset (and peers) back to their wire dictionaries before the aggregate union validates them. This keeps the public constructors composable without relaxing the on-wire discriminator checks.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var model_config

Inherited members

class CreativeStatus (*args, **kwds)
Expand source code
class CreativeStatus(StrEnum):
    processing = 'processing'
    pending_review = 'pending_review'
    approved = 'approved'
    suspended = 'suspended'
    rejected = 'rejected'
    archived = 'archived'

Enum where members are also (and must be) strings

Ancestors

  • enum.StrEnum
  • builtins.str
  • enum.ReprEnum
  • enum.Enum

Class variables

var approved
var archived
var pending_review
var processing
var rejected
var suspended
class CreativeVariant (**data: Any)

Base class enforcing the primary canonical runtime boundary.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var brand_lift : float | None
var brand_search_lift : float | None
var by_action_source : list[adcp.types.generated_poc.core.delivery_metrics.ByActionSourceItem] | None
var by_event_type : list[adcp.types.generated_poc.core.delivery_metrics.ByEventTypeItem] | None
var clicks : float | None
var commissionable_value : float | None
var completed_views : float | None
var completion_rate : float | None
var conversion_lift : float | None
var conversion_value : float | None
var conversions : float | None
var cost_per_acquisition : float | None
var cost_per_click : float | None
var cost_per_completed_view : float | None
var cpm : float | None
var ctr : float | None
var dooh_metrics : adcp.types.generated_poc.core.delivery_metrics.DoohMetrics | None
var downloads : float | None
var engagement_rate : float | None
var engagements : float | None
var follows : float | None
var foot_traffic : float | None
var frequency : float | None
var generation_context : adcp.types.generated_poc.core.creative_variant.GenerationContext | None
var grps : float | None
var impressions : float | None
var incremental_sales_lift : float | None
var leads : float | None
var locale_variant_id : str | None
var manifestCreativeManifest | None
var model_config
var new_to_brand_rate : float | None
var new_to_brand_units : float | None
var plays : float | None
var profile_visits : float | None
var quartile_data : adcp.types.generated_poc.core.delivery_metrics.QuartileData | None
var reach : float | None
var reach_unit : adcp.types.generated_poc.enums.reach_unit.ReachUnit | None
var reach_window : adcp.types.generated_poc.core.delivery_metrics.ReachWindow | None
var roas : float | None
var saves : float | None
var spend : float | None
var units_sold : float | None
var variant_id : str
var vendor_metric_values : list[adcp.types.generated_poc.core.vendor_metric_value.VendorMetricValue] | None
var viewability : adcp.types.generated_poc.core.delivery_metrics.Viewability | None
var views : float | None

Inherited members

class CssContent (**data: Any)
Expand source code
class CssAsset(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    asset_type: Annotated[
        Literal['css'],
        Field(
            description='Discriminator identifying this as a CSS asset. See /schemas/creative/asset-types for the registry.'
        ),
    ] = 'css'
    content: Annotated[str, Field(description='CSS content')]
    media: Annotated[
        str | None, Field(description="CSS media query context (e.g., 'screen', 'print')")
    ] = None
    provenance: Annotated[
        provenance_1.Provenance | None,
        Field(
            description='Provenance metadata for this asset, overrides manifest-level provenance'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var asset_type : Literal['css']
var content : str
var media : str | None
var model_config
var provenance : adcp.types.generated_poc.core.provenance.Provenance | None

Inherited members

class CursorStore (*args, **kwargs)
Expand source code
@runtime_checkable
class CursorStore(Protocol):
    """Protocol for persisting the feed cursor."""

    async def load(self) -> str | None:
        """Load the saved cursor, or None if no cursor exists."""
        ...

    async def save(self, cursor: str) -> None:
        """Save the current cursor."""
        ...

Protocol for persisting the feed cursor.

Ancestors

  • typing.Protocol
  • typing.Generic

Methods

async def load(self) ‑> str | None
Expand source code
async def load(self) -> str | None:
    """Load the saved cursor, or None if no cursor exists."""
    ...

Load the saved cursor, or None if no cursor exists.

async def save(self, cursor: str) ‑> None
Expand source code
async def save(self, cursor: str) -> None:
    """Save the current cursor."""
    ...

Save the current cursor.

class DaastAsset (root: RootModelRootType = PydanticUndefined, **data)
Expand source code
class DaastAsset(RootModel[DaastAsset3 | DaastAsset4]):
    root: Annotated[
        DaastAsset3 | DaastAsset4,
        Field(
            description='DAAST (Digital Audio Ad Serving Template) tag for third-party audio ad serving',
            discriminator='delivery_type',
            title='DAAST Asset',
        ),
    ]
    def __getattr__(self, name: str) -> Any:
        """Proxy attribute access to the wrapped type."""
        if name.startswith('_'):
            raise AttributeError(name)
        return getattr(self.root, name)

Usage Documentation

RootModel and Custom Root Types

A Pydantic BaseModel for the root object of the model.

Attributes
-----=
root
The root object of the model.
__pydantic_root_model__
Whether the model is a RootModel.
__pydantic_private__
Private fields in the model.
__pydantic_extra__
Extra fields in the model.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • pydantic.root_model.RootModel[Union[DaastAsset3, DaastAsset4]]
  • pydantic.root_model.RootModel
  • pydantic.main.BaseModel
  • typing.Generic

Class variables

var model_config
var root : adcp.types.generated_poc.core.assets.daast_asset.DaastAsset3 | adcp.types.generated_poc.core.assets.daast_asset.DaastAsset4
class UrlDaastAsset (**data: Any)
Expand source code
class DaastAsset1(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    asset_type: Annotated[
        Literal['daast'],
        Field(
            description='Discriminator identifying this as a DAAST asset. See /schemas/creative/asset-types for the registry.'
        ),
    ] = 'daast'
    daast_version: Annotated[
        DaastVersion | None, Field(description='DAAST specification version')
    ] = None
    duration_ms: Annotated[
        int | None, Field(description='Expected audio duration in milliseconds (if known)', ge=0)
    ] = None
    tracking_events: Annotated[
        list[DaastTrackingEvent] | None,
        Field(description='Tracking events supported by this DAAST tag'),
    ] = None
    companion_ads: Annotated[
        bool | None, Field(description='Whether companion display ads are included')
    ] = None
    transcript_url: Annotated[
        AnyUrl | None, Field(description='URL to text transcript of the audio content')
    ] = None
    provenance: Annotated[
        Provenance | None,
        Field(
            description='Provenance metadata for this asset, overrides manifest-level provenance'
        ),
    ] = None
    delivery_type: Annotated[
        Literal['url'],
        Field(description='Discriminator indicating DAAST is delivered via URL endpoint'),
    ] = 'url'
    url: Annotated[
        str,
        Field(
            description='URL endpoint that returns DAAST XML. May carry unsubstituted ad-server macros — DAAST/VAST-style `[MACRO]` and `${MACRO}` placeholders are accepted as-is (RFC 6570 syntax); buyers MUST NOT pre-encode macro delimiters, since players match the literal token at substitution time.'
        ),
    ]

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var asset_type : Literal['daast']
var companion_ads : bool | None
var daast_version : adcp.types.generated_poc.core.assets.asset_union.DaastVersion | None
var delivery_type : Literal['url']
var duration_ms : int | None
var model_config
var provenance : adcp.types.generated_poc.core.assets.asset_union.Provenance | None
var tracking_events : list[adcp.types.generated_poc.core.assets.asset_union.DaastTrackingEvent] | None
var transcript_url : pydantic.networks.AnyUrl | None
var url : str

Inherited members

class InlineDaastAsset (**data: Any)
Expand source code
class DaastAsset2(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    asset_type: Annotated[
        Literal['daast'],
        Field(
            description='Discriminator identifying this as a DAAST asset. See /schemas/creative/asset-types for the registry.'
        ),
    ] = 'daast'
    daast_version: Annotated[
        DaastVersion | None, Field(description='DAAST specification version')
    ] = None
    duration_ms: Annotated[
        int | None, Field(description='Expected audio duration in milliseconds (if known)', ge=0)
    ] = None
    tracking_events: Annotated[
        list[DaastTrackingEvent] | None,
        Field(description='Tracking events supported by this DAAST tag'),
    ] = None
    companion_ads: Annotated[
        bool | None, Field(description='Whether companion display ads are included')
    ] = None
    transcript_url: Annotated[
        AnyUrl | None, Field(description='URL to text transcript of the audio content')
    ] = None
    provenance: Annotated[
        Provenance | None,
        Field(
            description='Provenance metadata for this asset, overrides manifest-level provenance'
        ),
    ] = None
    delivery_type: Annotated[
        Literal['inline'],
        Field(description='Discriminator indicating DAAST is delivered as inline XML content'),
    ] = 'inline'
    content: Annotated[str, Field(description='Inline DAAST XML content')]

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var asset_type : Literal['daast']
var companion_ads : bool | None
var content : str
var daast_version : adcp.types.generated_poc.core.assets.asset_union.DaastVersion | None
var delivery_type : Literal['inline']
var duration_ms : int | None
var model_config
var provenance : adcp.types.generated_poc.core.assets.asset_union.Provenance | None
var tracking_events : list[adcp.types.generated_poc.core.assets.asset_union.DaastTrackingEvent] | None
var transcript_url : pydantic.networks.AnyUrl | None

Inherited members

class DaastTrackerAsset (**data: Any)
Expand source code
class DaastTrackerAsset(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    asset_type: Annotated[
        Literal['daast_tracker'],
        Field(
            description='Discriminator identifying this as a DAAST tracker asset. See /schemas/creative/asset-types for the registry.'
        ),
    ] = 'daast_tracker'
    daast_event: Annotated[
        daast_tracking_event.DaastTrackingEvent,
        Field(
            description='The DAAST tracking event this URL fires on. MUST NOT be `impression` (model as `url` asset with `url_type: "tracker_pixel"`), `clickTracking` / `customClick` (click-tracking trackers go on their own URL asset), `error`, or any of the `ViewableImpression`-element children (`viewable`, `notViewable`, `viewUndetermined`, `measurableImpression`, `viewableImpression`).'
        ),
    ]
    url: Annotated[
        str,
        Field(
            description='Tracker URL that fires when `daast_event` occurs. May carry AdCP universal macros; the sales agent or ad server URL-encodes substituted values at serve time. See docs/creative/universal-macros.mdx.'
        ),
    ]
    offset: Annotated[
        str | None,
        Field(
            description='DAAST `offset` attribute. Required when `daast_event` is `progress` (DAAST 1.1 §3.2.4.3); ignored otherwise. Same format as VAST 4.2 `Tracking@offset`: `HH:MM:SS` or `HH:MM:SS.mmm` for absolute time (two-digit hours, minutes 00–59, seconds 00–59), or an integer percentage 0–100 suffixed with `%`. Negative offsets are NOT permitted.',
            pattern='^(\\d{2}:[0-5]\\d:[0-5]\\d(\\.\\d{3})?|(100|\\d{1,2})%)$',
        ),
    ] = None
    target: Annotated[
        Target | None,
        Field(
            description='Which DAAST creative element this tracker scopes to — `linear` for `<Linear>/<TrackingEvents>` (DAAST 1.1 §3.2.1.7), `companion` for `<CompanionAds>/<Companion>/<TrackingEvents>` (DAAST 1.1 §3.2.2.7, where the only valid event is `creativeView`). DAAST has no `<NonLinearAds>` element. Defaults to `linear`. Sales agents use this to place the tracker in the correct location during DAAST assembly.'
        ),
    ] = Target.linear
    provenance: Annotated[
        provenance_1.Provenance | None,
        Field(
            description='Provenance metadata for this asset, overrides manifest-level provenance.'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var asset_type : Literal['daast_tracker']
var daast_event : adcp.types.generated_poc.enums.daast_tracking_event.DaastTrackingEvent
var model_config
var offset : str | None
var provenance : adcp.types.generated_poc.core.provenance.Provenance | None
var target : adcp.types.generated_poc.core.assets.daast_tracker_asset.Target | None
var url : str

Inherited members

class DateRange (**data: Any)
Expand source code
class DateRange(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    start: Annotated[date, Field(description='Start date (inclusive), ISO 8601')]
    end: Annotated[date, Field(description='End date (inclusive), ISO 8601')]

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var end : datetime.date
var model_config
var start : datetime.date

Inherited members

class DatetimeRange (**data: Any)
Expand source code
class DatetimeRange(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    start: Annotated[AwareDatetime, Field(description='Start timestamp (inclusive), ISO 8601')]
    end: Annotated[AwareDatetime, Field(description='End timestamp (inclusive), ISO 8601')]

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var end : pydantic.types.AwareDatetime
var model_config
var start : pydantic.types.AwareDatetime

Inherited members

class DeclineProposalsRequest (**data: Any)
Expand source code
class DeclineProposalsRequest(AdCPBaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    adcp_version: version_envelope.AdcpVersion | None = None
    adcp_major_version: version_envelope.AdcpMajorVersion | None = None
    context_id: Annotated[str | None, Field(min_length=1)] = None
    context: context_1.ContextObject | None = None
    governance_context: Annotated[str | None, Field(max_length=4096, min_length=1)] = None
    push_notification_config: push_notification_config_1.PushNotificationConfig | None = None
    idempotency_key: Annotated[
        str,
        Field(
            description='Client-generated key required for retry-safe proposal decline.',
            max_length=255,
            min_length=16,
            pattern='^[A-Za-z0-9_.:-]{16,255}$',
        ),
    ]
    declines: Annotated[
        list[proposal_decline.ProposalDecline],
        Field(
            description='Proposal declines to apply. proposal_id is the semantic uniqueness key and values MUST be unique even when two entries otherwise differ; implementations enforce this rule because JSON Schema uniqueItems only compares whole objects. Results preserve request order.',
            max_length=25,
            min_length=1,
        ),
    ]
    opportunity: Annotated[
        opportunity_context.OpportunityContext | None,
        Field(
            description='Optional planning-cycle update. Every named proposal MUST belong to this opportunity_id. Sellers apply the update only when every result is declined; if any result is unable, the opportunity remains unchanged. Use status closed when these declines end the broader opportunity.'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var adcp_major_version : adcp.types.generated_poc.core.version_envelope.AdcpMajorVersion | None
var adcp_version : adcp.types.generated_poc.core.version_envelope.AdcpVersion | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var context_id : str | None
var declines : list[adcp.types.generated_poc.media_buy.proposal_decline.ProposalDecline]
var governance_context : str | None
var idempotency_key : str
var model_config
var opportunity : adcp.types.generated_poc.core.opportunity_context.OpportunityContext | None
var push_notification_config : adcp.types.generated_poc.core.push_notification_config.PushNotificationConfig | None

Inherited members

class DeliveryStatus (*args, **kwds)
Expand source code
class DeliveryStatus(StrEnum):
    delivering = 'delivering'
    not_delivering = 'not_delivering'
    completed = 'completed'
    budget_exhausted = 'budget_exhausted'
    flight_ended = 'flight_ended'
    goal_met = 'goal_met'

Enum where members are also (and must be) strings

Ancestors

  • enum.StrEnum
  • builtins.str
  • enum.ReprEnum
  • enum.Enum

Class variables

var budget_exhausted
var completed
var delivering
var flight_ended
var goal_met
var not_delivering
class PlatformDeployment (**data: Any)
Expand source code
class Deployment1(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    type: Annotated[
        Literal['platform'],
        Field(description='Discriminator indicating this is a platform-based deployment'),
    ] = 'platform'
    platform: Annotated[str, Field(description='Platform identifier for DSPs')]
    account: Annotated[str | None, Field(description='Account identifier if applicable')] = None
    is_live: Annotated[
        bool, Field(description='Whether signal is currently active on this deployment')
    ]
    activation_key: Annotated[
        activation_key_1.ActivationKey | None,
        Field(
            description='The key to use for targeting. Only present if is_live=true AND requester has access to this deployment.'
        ),
    ] = None
    estimated_activation_duration_minutes: Annotated[
        float | None,
        Field(
            description='Estimated time to activate if not live, or to complete activation if in progress',
            ge=0.0,
        ),
    ] = None
    deployed_at: Annotated[
        AwareDatetime | None,
        Field(description='Timestamp when activation completed (if is_live=true)'),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var account : str | None
var activation_key : adcp.types.generated_poc.core.activation_key.ActivationKey | None
var deployed_at : pydantic.types.AwareDatetime | None
var estimated_activation_duration_minutes : float | None
var is_live : bool
var model_config
var platform : str
var type : Literal['platform']

Inherited members

class AgentDeployment (**data: Any)
Expand source code
class Deployment2(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    type: Annotated[
        Literal['agent'],
        Field(description='Discriminator indicating this is an agent URL-based deployment'),
    ] = 'agent'
    agent_url: Annotated[AnyUrl, Field(description='URL identifying the deployment agent')]
    account: Annotated[str | None, Field(description='Account identifier if applicable')] = None
    is_live: Annotated[
        bool, Field(description='Whether signal is currently active on this deployment')
    ]
    activation_key: Annotated[
        activation_key_1.ActivationKey | None,
        Field(
            description='The key to use for targeting. Only present if is_live=true AND requester has access to this deployment.'
        ),
    ] = None
    estimated_activation_duration_minutes: Annotated[
        float | None,
        Field(
            description='Estimated time to activate if not live, or to complete activation if in progress',
            ge=0.0,
        ),
    ] = None
    deployed_at: Annotated[
        AwareDatetime | None,
        Field(description='Timestamp when activation completed (if is_live=true)'),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var account : str | None
var activation_key : adcp.types.generated_poc.core.activation_key.ActivationKey | None
var agent_url : pydantic.networks.AnyUrl
var deployed_at : pydantic.types.AwareDatetime | None
var estimated_activation_duration_minutes : float | None
var is_live : bool
var model_config
var type : Literal['agent']

Inherited members

class PlatformDestination (**data: Any)
Expand source code
class Destination1(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    type: Annotated[
        Literal['platform'],
        Field(description='Discriminator indicating this is a platform-based deployment'),
    ] = 'platform'
    platform: Annotated[
        str,
        Field(description="Platform identifier for DSPs (e.g., 'the-trade-desk', 'amazon-dsp')"),
    ]
    account: Annotated[
        str | None, Field(description='Optional account identifier on the platform')
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var account : str | None
var model_config
var platform : str
var type : Literal['platform']

Inherited members

class AgentDestination (**data: Any)
Expand source code
class Destination2(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    type: Annotated[
        Literal['agent'],
        Field(description='Discriminator indicating this is an agent URL-based deployment'),
    ] = 'agent'
    agent_url: Annotated[
        AnyUrl, Field(description='URL identifying the deployment agent (for sales agents, etc.)')
    ]
    account: Annotated[
        str | None, Field(description='Optional account identifier on the agent')
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var account : str | None
var agent_url : pydantic.networks.AnyUrl
var model_config
var type : Literal['agent']

Inherited members

class DevicePlatform (*args, **kwds)
Expand source code
class DevicePlatform(StrEnum):
    ios = 'ios'
    android = 'android'
    windows = 'windows'
    macos = 'macos'
    linux = 'linux'
    chromeos = 'chromeos'
    tvos = 'tvos'
    tizen = 'tizen'
    webos = 'webos'
    fire_os = 'fire_os'
    roku_os = 'roku_os'
    unknown = 'unknown'

Enum where members are also (and must be) strings

Ancestors

  • enum.StrEnum
  • builtins.str
  • enum.ReprEnum
  • enum.Enum

Class variables

var android
var chromeos
var fire_os
var ios
var linux
var macos
var roku_os
var tizen
var tvos
var unknown
var webos
var windows
class DeviceType (*args, **kwds)
Expand source code
class DeviceType(StrEnum):
    desktop = 'desktop'
    mobile = 'mobile'
    tablet = 'tablet'
    ctv = 'ctv'
    dooh = 'dooh'
    unknown = 'unknown'

Enum where members are also (and must be) strings

Ancestors

  • enum.StrEnum
  • builtins.str
  • enum.ReprEnum
  • enum.Enum

Class variables

var ctv
var desktop
var dooh
var mobile
var tablet
var unknown
class DirectoryPublisherEntry (**data: Any)
Expand source code
class DirectoryPublisherEntry(AdCPBaseModel):
    """One publisher row in an AAO directory inverse-lookup response."""

    publisher_domain: str
    discovery_method: DirectoryDiscoveryMethod
    manager_domain: str | None = None
    properties_authorized: int = Field(ge=0)
    properties_total: int = Field(ge=0)
    signing_keys_pinned: bool | None = None
    status: DirectoryEdgeStatus
    last_verified_at: datetime
    property_ids: list[str] | None = Field(
        default=None,
        description=(
            "Canonical property IDs the agent's selectors resolve to under "
            "this publisher. Present iff the request was made with "
            "include=['properties'] AND the directory server supports it "
            "(per adcp#4894). None signals count-only mode for downstream "
            "consumers."
        ),
    )

One publisher row in an AAO directory inverse-lookup response.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var discovery_method : Literal['direct', 'authoritative_location', 'adagents_authoritative', 'ads_txt_managerdomain']
var last_verified_at : datetime.datetime
var manager_domain : str | None
var model_config
var properties_authorized : int
var properties_total : int
var property_ids : list[str] | None
var publisher_domain : str
var signing_keys_pinned : bool | None
var status : Literal['authorized', 'revoked']

Inherited members

class DomainLookupResult (**data: Any)
Expand source code
class DomainLookupResult(RegistryBaseModel):
    domain: Annotated[str, Field(examples=["examplepub.com"])]
    authorized_agents: list[DomainAuthorizedAgent]
    sales_agents_claiming: list[SalesAgentClaim]

Base model for registry API types.

Uses extra='allow' so that new fields from the registry API are preserved rather than dropped. This differs from AdCPBaseModel which defaults to extra='ignore' for protocol types.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var authorized_agents : list[DomainAuthorizedAgent]
var domain : str
var model_config
var sales_agents_claiming : list[SalesAgentClaim]
class DownstreamConnectionRequirement (**data: Any)
Expand source code
class DownstreamConnectionRequirement(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    provider: Annotated[
        str | None,
        Field(
            description='Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.'
        ),
    ] = None
    connection_type: Annotated[
        ConnectionType,
        Field(
            description='Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity.'
        ),
    ]
    required_for: Annotated[
        list[RequiredForItem] | None,
        Field(
            description='Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.'
        ),
    ] = None
    scope: Annotated[Scope | None, Field(description='Granularity of the downstream grant.')] = None
    status: Annotated[
        Status | None,
        Field(
            description='Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.'
        ),
    ] = None
    connection_id: Annotated[
        str | None,
        Field(
            description='Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.'
        ),
    ] = None
    resource_ref: Annotated[
        ResourceRef | None,
        Field(
            description='Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.'
        ),
    ] = None
    authorization_url: Annotated[
        AnyUrl | None,
        Field(
            description='Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.'
        ),
    ] = None
    authorization_instructions: Annotated[
        str | None,
        Field(
            description='Human-readable instructions for completing or restoring this downstream connection.'
        ),
    ] = None
    expires_at: Annotated[
        AwareDatetime | None,
        Field(description='Expiration time for the downstream grant, when known.'),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var authorization_instructions : str | None
var authorization_url : pydantic.networks.AnyUrl | None
var connection_id : str | None
var connection_type : adcp.types.generated_poc.core.downstream_connection_requirement.ConnectionType
var expires_at : pydantic.types.AwareDatetime | None
var model_config
var provider : str | None
var required_for : list[adcp.types.generated_poc.core.downstream_connection_requirement.RequiredForItem] | None
var resource_ref : adcp.types.generated_poc.core.downstream_connection_requirement.ResourceRef | None
var scope : adcp.types.generated_poc.core.downstream_connection_requirement.Scope | None
var status : adcp.types.generated_poc.core.downstream_connection_requirement.Status | None

Inherited members

class Duration (**data: Any)
Expand source code
class Duration(AdCPBaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    interval: Annotated[
        int, Field(description="Number of time units. Must be 1 when unit is 'campaign'.", ge=1)
    ]
    unit: Annotated[
        Unit,
        Field(
            description="Time unit. 'seconds' for sub-minute precision. 'campaign' spans the full campaign flight."
        ),
    ]

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Subclasses

  • adcp.types.generated_poc.core.audience_evidence_requirements.MaximumAge
  • adcp.types.generated_poc.core.product_audience_evidence_requirements.MaximumAge

Class variables

var interval : int
var model_config
var unit : adcp.types.generated_poc.core.duration.Unit

Inherited members

class Error (**data: Any)
Expand source code
class Error(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    code: Annotated[
        str,
        Field(
            description='Error code for programmatic handling. The error-code vocabulary is open: `error.code` is wire-typed `string` (not a closed enum), the standard codes published in `enums/error-code.json` are documentary, and senders MAY emit codes outside that set (platform-specific codes, or codes introduced in a later AdCP version). Receivers MUST decode unknown codes — treat the response as well-formed, read `error.recovery` for the recovery classification, and fall back to `transient` when `recovery` is absent. See `error-handling.mdx#forward-compatible-decoding-normative` for the full forward-compat contract — this rule is what lets future maintenance lines ship new codes additively.',
            max_length=64,
            min_length=1,
        ),
    ]
    message: Annotated[str, Field(description='Human-readable error message')]
    field: Annotated[
        str | None,
        Field(
            description="Field path associated with the error in JSONPath-lite format (e.g., 'packages[0].targeting'). When `issues[]` is also present, sellers MUST set this to `issues[0].pointer` translated from RFC 6901 to JSONPath-lite (e.g., '/packages/0/targeting' → 'packages[0].targeting') so pre-3.1 consumers reading `field` only get deterministic behavior. Will be deprecated in a future major version in favor of `issues[].pointer`."
        ),
    ] = None
    suggestion: Annotated[str | None, Field(description='Suggested fix for the error')] = None
    retry_after: Annotated[
        float | None,
        Field(
            description='Seconds to wait before retrying the operation. AdCP 3.2 producers MUST emit an integer from 1 through 3600. The 3.x schema continues to accept finite fractional values for backward compatibility with earlier producers; consumers receiving one MUST round up to the next whole second before clamping so the retry is never scheduled earlier than intended. Non-finite values are treated as absent.',
            ge=1.0,
            le=3600.0,
        ),
    ] = None
    issues: Annotated[
        list[Issue] | None,
        Field(
            description='Structured list of validation failures. Primary use is `VALIDATION_ERROR`, where multi-field rejections are common and `field` (singular) cannot carry the full pointer map. MAY appear on other error codes that reject multiple fields at once. When `issues` is present, sellers MUST also populate `field` from `issues[0]` for backward compatibility with pre-3.1 consumers that read `field` only — translating the RFC 6901 `pointer` format to the JSONPath-lite format `field` uses (e.g., `/packages/0/targeting` → `packages[0].targeting`). MUST (not SHOULD) so consumers reading `field` get deterministic behavior across sellers — the cost is one line of dual-write per seller; the cost of SHOULD is a long tail of seller-A-vs-seller-B inconsistency. Future major versions will deprecate `field` in favor of `issues[].pointer`.'
        ),
    ] = None
    details: Annotated[
        dict[str, Any] | None,
        Field(
            description='Additional task-specific error details. Sellers MAY mirror `issues[]` here as `details.issues` for backward compatibility with pre-3.1 consumers reading from `details`; new consumers SHOULD prefer the top-level `issues` field.\n\n**Canonical rejection-set shape (3.1+).** When the error reports a rejected value against a closed set of accepted values (e.g., enum mismatch, unsupported pricing option, invalid signal id), sellers SHOULD use the canonical key `accepted_values: <array>` under `details` rather than seller-specific variants observed in the wild (`available`, `allowed`, `accepted_values` at the error root, etc.). The canonical shape:\n\n```json\n{\n  "code": "INVALID_PRICING_OPTION",\n  "message": "Pricing option not found: po_prism_abandoner_cpm",\n  "field": "pricing_option_id",\n  "details": {\n    "rejected_value": "po_prism_abandoner_cpm",\n    "accepted_values": ["po_prism_cart_cpm", "po_prism_view_cpm"]\n  }\n}\n```\n\n- `rejected_value` (optional): the offending value the buyer supplied, echoed for buyer-side diagnostic clarity (especially when the offending field is nested or transformed before validation).\n- `accepted_values` (optional): the closed set the seller would have accepted at this field on this call. Sellers MUST NOT enumerate the full ecosystem-wide accepted set if it differs from what\'s accepted for *this caller in this context* (account, brand, scope) — leaking ecosystem-wide accepted sets to a per-caller rejection turns the error into an enumeration oracle.\n\nThis is **SHOULD-level guidance**, not MUST: `details` remains `additionalProperties: true` and pre-3.1 sellers using `available` / `allowed` / `accepted_values` at the error root remain conformant. The canonical shape lets buyer-side diagnostic tooling (SDK runner hints, dashboards, error classifiers) reliably surface the accepted-set without per-seller pattern matching. SDKs SHOULD accept any of the legacy variants and normalize on read; the canonical shape is what new sellers and 3.1+ adopters should emit going forward.'
        ),
    ] = None
    recovery: Annotated[
        Recovery | None,
        Field(
            description='Agent recovery classification. transient: retry after delay (rate limit, service unavailable, timeout). correctable: fix the request and resend (invalid field, budget too low, creative rejected). terminal: requires human action (account suspended, payment required, account not found). Senders SHOULD populate `recovery` on every error from 3.1 onward — it is the normative carrier of recovery semantics across version skew. A receiver that does not recognize `error.code` (a newer code, or a platform-specific code) MUST still be able to classify the error from `recovery`. The `enumMetadata.recovery` block in `enums/error-code.json` is the documentary mirror for known codes; `error.recovery` on the wire is authoritative.'
        ),
    ] = None
    source: Annotated[
        Source | None,
        Field(
            description='Who emitted this error entry. `producer` (default when absent): emitted by the response\'s authoring agent (the seller for `get_products`, the creative agent for `build_creative`, etc.). `sdk`: augmented by a consuming SDK that detected a non-fatal advisory condition on consumption (e.g., `FORMAT_PROJECTION_FAILED` when the buyer SDK couldn\'t project a v1 format to a canonical, or `FORMAT_DECLARATION_DIVERGENT` when the SDK detected a producer bug on read). SDK-augmented entries SHOULD also set `sdk_id` so downstream consumers can identify which intermediate processor inserted the entry.\n\n**Multi-hop propagation (normative).** AdCP is a federated agent network — responses commonly traverse multiple SDKs (e.g., sales agent → interchange → DSP → buyer). When an SDK augments `errors[]` with a consumption-detected entry, the augmented response carries the entry forward to subsequent hops. Each hop that detects the same condition independently SHOULD deduplicate by `(code, field)` rather than re-emit; the existing entry\'s `sdk_id` identifies which earlier processor saw it first. Producer entries (those without `source: "sdk"`) are authoritative for what the response\'s authoring agent self-detected; SDK entries are observations made on top.\n\n**Replay/audit safety.** Persisted or replayed responses carry `source` and `sdk_id` so the audit trail can distinguish seller-emitted entries from SDK-augmented ones. Without `source`, a downstream consumer can\'t tell whether a code came from the seller or an intermediate SDK, which corrupts attribution.'
        ),
    ] = None
    sdk_id: Annotated[
        str | None,
        Field(
            description='Optional identifier for the SDK that augmented this error entry. Format: `<sdk_package_name>@<version>` (e.g., `@adcontextprotocol/adcp@7.3.0`, `adcontextprotocol-adcp-python@1.2.0`). MUST be set when `source: "sdk"`; MUST be absent when `source: "producer"` or absent. Lets downstream consumers identify which intermediate processor inserted the entry, useful for debugging cross-SDK divergence (e.g., one SDK detects a projection failure that another SDK\'s registry version doesn\'t).'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Subclasses

  • adcp.types.generated_poc.core.catalog_item_availability_error.CatalogItemAvailabilityError

Class variables

var code : str
var details : dict[str, typing.Any] | None
var field : str | None
var issues : list[adcp.types.generated_poc.core.error.Issue] | None
var message : str
var model_config
var recovery : adcp.types.generated_poc.core.error.Recovery | None
var retry_after : float | None
var sdk_id : str | None
var source : adcp.types.generated_poc.core.error.Source | None
var suggestion : str | None

Inherited members

class ErrorCode (*args, **kwds)
Expand source code
class ErrorCode(StrEnum):
    INVALID_REQUEST = 'INVALID_REQUEST'
    AUTH_REQUIRED = 'AUTH_REQUIRED'
    AUTH_MISSING = 'AUTH_MISSING'
    AUTH_INVALID = 'AUTH_INVALID'
    AUTHORIZATION_REQUIRED = 'AUTHORIZATION_REQUIRED'
    RATE_LIMITED = 'RATE_LIMITED'
    SERVICE_UNAVAILABLE = 'SERVICE_UNAVAILABLE'
    CONFIGURATION_ERROR = 'CONFIGURATION_ERROR'
    POLICY_VIOLATION = 'POLICY_VIOLATION'
    PRODUCT_NOT_FOUND = 'PRODUCT_NOT_FOUND'
    PRODUCT_UNAVAILABLE = 'PRODUCT_UNAVAILABLE'
    PROPOSAL_EXPIRED = 'PROPOSAL_EXPIRED'
    BUDGET_TOO_LOW = 'BUDGET_TOO_LOW'
    CREATIVE_REJECTED = 'CREATIVE_REJECTED'
    CREATIVE_LOCALE_NOT_ACCEPTED = 'CREATIVE_LOCALE_NOT_ACCEPTED'
    CREATIVE_VALUE_NOT_ALLOWED = 'CREATIVE_VALUE_NOT_ALLOWED'
    UNSUPPORTED_FEATURE = 'UNSUPPORTED_FEATURE'
    UNPRICEABLE_OUTPUT = 'UNPRICEABLE_OUTPUT'
    UNSUPPORTED_GRANULARITY = 'UNSUPPORTED_GRANULARITY'
    UNSUPPORTED_PROVISIONING = 'UNSUPPORTED_PROVISIONING'
    AUDIENCE_TOO_SMALL = 'AUDIENCE_TOO_SMALL'
    ACCOUNT_REQUIRED = 'ACCOUNT_REQUIRED'
    ACCOUNT_NOT_FOUND = 'ACCOUNT_NOT_FOUND'
    ACCOUNT_MOVED = 'ACCOUNT_MOVED'
    ACCOUNT_IDENTITY_CONFLICT = 'ACCOUNT_IDENTITY_CONFLICT'
    ACCOUNT_SETUP_REQUIRED = 'ACCOUNT_SETUP_REQUIRED'
    ACCOUNT_AMBIGUOUS = 'ACCOUNT_AMBIGUOUS'
    ACCOUNT_PAYMENT_REQUIRED = 'ACCOUNT_PAYMENT_REQUIRED'
    ACCOUNT_SUSPENDED = 'ACCOUNT_SUSPENDED'
    COMPLIANCE_UNSATISFIED = 'COMPLIANCE_UNSATISFIED'
    GOVERNANCE_DENIED = 'GOVERNANCE_DENIED'
    BUDGET_EXHAUSTED = 'BUDGET_EXHAUSTED'
    BUDGET_EXCEEDED = 'BUDGET_EXCEEDED'
    BUDGET_CAP_REACHED = 'BUDGET_CAP_REACHED'
    CONFLICT = 'CONFLICT'
    IDEMPOTENCY_CONFLICT = 'IDEMPOTENCY_CONFLICT'
    IDEMPOTENCY_EXPIRED = 'IDEMPOTENCY_EXPIRED'
    IDEMPOTENCY_IN_FLIGHT = 'IDEMPOTENCY_IN_FLIGHT'
    CREATIVE_DEADLINE_EXCEEDED = 'CREATIVE_DEADLINE_EXCEEDED'
    CREATIVE_INACCESSIBLE = 'CREATIVE_INACCESSIBLE'
    INVALID_STATE = 'INVALID_STATE'
    MEDIA_BUY_NOT_FOUND = 'MEDIA_BUY_NOT_FOUND'
    NOT_CANCELLABLE = 'NOT_CANCELLABLE'
    PACKAGE_NOT_FOUND = 'PACKAGE_NOT_FOUND'
    PLACE_TARGET_UNAVAILABLE = 'PLACE_TARGET_UNAVAILABLE'
    CREATIVE_NOT_FOUND = 'CREATIVE_NOT_FOUND'
    SIGNAL_NOT_FOUND = 'SIGNAL_NOT_FOUND'
    SIGNAL_TARGETING_INCOMPATIBLE = 'SIGNAL_TARGETING_INCOMPATIBLE'
    SESSION_NOT_FOUND = 'SESSION_NOT_FOUND'
    PLAN_NOT_FOUND = 'PLAN_NOT_FOUND'
    REFERENCE_NOT_FOUND = 'REFERENCE_NOT_FOUND'
    SESSION_TERMINATED = 'SESSION_TERMINATED'
    VALIDATION_ERROR = 'VALIDATION_ERROR'
    PRODUCT_EXPIRED = 'PRODUCT_EXPIRED'
    PROPOSAL_NOT_COMMITTED = 'PROPOSAL_NOT_COMMITTED'
    PROPOSAL_NOT_FOUND = 'PROPOSAL_NOT_FOUND'
    MULTI_FINALIZE_UNSUPPORTED = 'MULTI_FINALIZE_UNSUPPORTED'
    IO_REQUIRED = 'IO_REQUIRED'
    TERMS_REJECTED = 'TERMS_REJECTED'
    BIDDING_PLACEMENT_CONFLICT = 'BIDDING_PLACEMENT_CONFLICT'
    AMBIGUOUS_BIDDING_POLICY = 'AMBIGUOUS_BIDDING_POLICY'
    CONFLICTING_SELECTORS = 'CONFLICTING_SELECTORS'
    REQUOTE_REQUIRED = 'REQUOTE_REQUIRED'
    VERSION_UNSUPPORTED = 'VERSION_UNSUPPORTED'
    CAMPAIGN_SUSPENDED = 'CAMPAIGN_SUSPENDED'
    GOVERNANCE_UNAVAILABLE = 'GOVERNANCE_UNAVAILABLE'
    PERMISSION_DENIED = 'PERMISSION_DENIED'
    SCOPE_INSUFFICIENT = 'SCOPE_INSUFFICIENT'
    READ_ONLY_SCOPE = 'READ_ONLY_SCOPE'
    FIELD_NOT_PERMITTED = 'FIELD_NOT_PERMITTED'
    PROVENANCE_REQUIRED = 'PROVENANCE_REQUIRED'
    PROVENANCE_DIGITAL_SOURCE_TYPE_MISSING = 'PROVENANCE_DIGITAL_SOURCE_TYPE_MISSING'
    PROVENANCE_SYNTHETIC_DEPICTION_MISSING = 'PROVENANCE_SYNTHETIC_DEPICTION_MISSING'
    PROVENANCE_DISCLOSURE_MISSING = 'PROVENANCE_DISCLOSURE_MISSING'
    PROVENANCE_EMBEDDED_MISSING = 'PROVENANCE_EMBEDDED_MISSING'
    PROVENANCE_VERIFIER_NOT_ACCEPTED = 'PROVENANCE_VERIFIER_NOT_ACCEPTED'
    PROVENANCE_CLAIM_CONTRADICTED = 'PROVENANCE_CLAIM_CONTRADICTED'
    EVALUATOR_AGENT_NOT_ACCEPTED = 'EVALUATOR_AGENT_NOT_ACCEPTED'
    BILLING_NOT_SUPPORTED = 'BILLING_NOT_SUPPORTED'
    BILLING_NOT_PERMITTED_FOR_AGENT = 'BILLING_NOT_PERMITTED_FOR_AGENT'
    BILLING_OUT_OF_BAND = 'BILLING_OUT_OF_BAND'
    PAYMENT_TERMS_NOT_SUPPORTED = 'PAYMENT_TERMS_NOT_SUPPORTED'
    BRAND_REQUIRED = 'BRAND_REQUIRED'
    AGENT_SUSPENDED = 'AGENT_SUSPENDED'
    AGENT_BLOCKED = 'AGENT_BLOCKED'
    CREDENTIAL_IN_ARGS = 'CREDENTIAL_IN_ARGS'
    ACTION_NOT_ALLOWED = 'ACTION_NOT_ALLOWED'
    PRIVATE_FIELD_IN_PUBLIC_PLACEMENT = 'PRIVATE_FIELD_IN_PUBLIC_PLACEMENT'
    FORMAT_PROJECTION_FAILED = 'FORMAT_PROJECTION_FAILED'
    FORMAT_DECLARATION_DIVERGENT = 'FORMAT_DECLARATION_DIVERGENT'
    FORMAT_DECLARATION_V1_AMBIGUOUS = 'FORMAT_DECLARATION_V1_AMBIGUOUS'
    FORMAT_OPTION_UNRESOLVED = 'FORMAT_OPTION_UNRESOLVED'
    FORMAT_DECLARATION_V1_LOSSY_MULTI_SIZE = 'FORMAT_DECLARATION_V1_LOSSY_MULTI_SIZE'
    FORMAT_NOT_SUPPORTED = 'FORMAT_NOT_SUPPORTED'
    PIXEL_TRACKER_LOSSY_DOWNGRADE = 'PIXEL_TRACKER_LOSSY_DOWNGRADE'
    PIXEL_TRACKER_UPGRADE_INFERRED = 'PIXEL_TRACKER_UPGRADE_INFERRED'
    STALE_RESPONSE = 'STALE_RESPONSE'
    FEED_FETCH_FAILED = 'FEED_FETCH_FAILED'
    INVALID_FEED_FORMAT = 'INVALID_FEED_FORMAT'
    ITEM_VALIDATION_FAILED = 'ITEM_VALIDATION_FAILED'
    CATALOG_LIMIT_EXCEEDED = 'CATALOG_LIMIT_EXCEEDED'
    INVALID_PRICING_OPTION = 'INVALID_PRICING_OPTION'
    INVALID_USAGE_DATA = 'INVALID_USAGE_DATA'
    SIGNED_RESPONSE_ENVELOPE_EXPIRED = 'SIGNED_RESPONSE_ENVELOPE_EXPIRED'
    SIGNED_RESPONSE_REQUEST_HASH_MISMATCH = 'SIGNED_RESPONSE_REQUEST_HASH_MISMATCH'
    SIGNED_RESPONSE_TENANT_MISMATCH = 'SIGNED_RESPONSE_TENANT_MISMATCH'
    VAST_PARSE_FAILED = 'VAST_PARSE_FAILED'
    VAST_VERSION_MISMATCH = 'VAST_VERSION_MISMATCH'
    VAST_WRAPPER_DEPTH_EXCEEDED = 'VAST_WRAPPER_DEPTH_EXCEEDED'

Enum where members are also (and must be) strings

Ancestors

  • enum.StrEnum
  • builtins.str
  • enum.ReprEnum
  • enum.Enum

Class variables

var ACCOUNT_AMBIGUOUS
var ACCOUNT_IDENTITY_CONFLICT
var ACCOUNT_MOVED
var ACCOUNT_NOT_FOUND
var ACCOUNT_PAYMENT_REQUIRED
var ACCOUNT_REQUIRED
var ACCOUNT_SETUP_REQUIRED
var ACCOUNT_SUSPENDED
var ACTION_NOT_ALLOWED
var AGENT_BLOCKED
var AGENT_SUSPENDED
var AMBIGUOUS_BIDDING_POLICY
var AUDIENCE_TOO_SMALL
var AUTHORIZATION_REQUIRED
var AUTH_INVALID
var AUTH_MISSING
var AUTH_REQUIRED
var BIDDING_PLACEMENT_CONFLICT
var BILLING_NOT_PERMITTED_FOR_AGENT
var BILLING_NOT_SUPPORTED
var BILLING_OUT_OF_BAND
var BRAND_REQUIRED
var BUDGET_CAP_REACHED
var BUDGET_EXCEEDED
var BUDGET_EXHAUSTED
var BUDGET_TOO_LOW
var CAMPAIGN_SUSPENDED
var CATALOG_LIMIT_EXCEEDED
var COMPLIANCE_UNSATISFIED
var CONFIGURATION_ERROR
var CONFLICT
var CONFLICTING_SELECTORS
var CREATIVE_DEADLINE_EXCEEDED
var CREATIVE_INACCESSIBLE
var CREATIVE_LOCALE_NOT_ACCEPTED
var CREATIVE_NOT_FOUND
var CREATIVE_REJECTED
var CREATIVE_VALUE_NOT_ALLOWED
var CREDENTIAL_IN_ARGS
var EVALUATOR_AGENT_NOT_ACCEPTED
var FEED_FETCH_FAILED
var FIELD_NOT_PERMITTED
var FORMAT_DECLARATION_DIVERGENT
var FORMAT_DECLARATION_V1_AMBIGUOUS
var FORMAT_DECLARATION_V1_LOSSY_MULTI_SIZE
var FORMAT_NOT_SUPPORTED
var FORMAT_OPTION_UNRESOLVED
var FORMAT_PROJECTION_FAILED
var GOVERNANCE_DENIED
var GOVERNANCE_UNAVAILABLE
var IDEMPOTENCY_CONFLICT
var IDEMPOTENCY_EXPIRED
var IDEMPOTENCY_IN_FLIGHT
var INVALID_FEED_FORMAT
var INVALID_PRICING_OPTION
var INVALID_REQUEST
var INVALID_STATE
var INVALID_USAGE_DATA
var IO_REQUIRED
var ITEM_VALIDATION_FAILED
var MEDIA_BUY_NOT_FOUND
var MULTI_FINALIZE_UNSUPPORTED
var NOT_CANCELLABLE
var PACKAGE_NOT_FOUND
var PAYMENT_TERMS_NOT_SUPPORTED
var PERMISSION_DENIED
var PIXEL_TRACKER_LOSSY_DOWNGRADE
var PIXEL_TRACKER_UPGRADE_INFERRED
var PLACE_TARGET_UNAVAILABLE
var PLAN_NOT_FOUND
var POLICY_VIOLATION
var PRIVATE_FIELD_IN_PUBLIC_PLACEMENT
var PRODUCT_EXPIRED
var PRODUCT_NOT_FOUND
var PRODUCT_UNAVAILABLE
var PROPOSAL_EXPIRED
var PROPOSAL_NOT_COMMITTED
var PROPOSAL_NOT_FOUND
var PROVENANCE_CLAIM_CONTRADICTED
var PROVENANCE_DIGITAL_SOURCE_TYPE_MISSING
var PROVENANCE_DISCLOSURE_MISSING
var PROVENANCE_EMBEDDED_MISSING
var PROVENANCE_REQUIRED
var PROVENANCE_SYNTHETIC_DEPICTION_MISSING
var PROVENANCE_VERIFIER_NOT_ACCEPTED
var RATE_LIMITED
var READ_ONLY_SCOPE
var REFERENCE_NOT_FOUND
var REQUOTE_REQUIRED
var SCOPE_INSUFFICIENT
var SERVICE_UNAVAILABLE
var SESSION_NOT_FOUND
var SESSION_TERMINATED
var SIGNAL_NOT_FOUND
var SIGNAL_TARGETING_INCOMPATIBLE
var SIGNED_RESPONSE_ENVELOPE_EXPIRED
var SIGNED_RESPONSE_REQUEST_HASH_MISMATCH
var SIGNED_RESPONSE_TENANT_MISMATCH
var STALE_RESPONSE
var TERMS_REJECTED
var UNPRICEABLE_OUTPUT
var UNSUPPORTED_FEATURE
var UNSUPPORTED_GRANULARITY
var UNSUPPORTED_PROVISIONING
var VALIDATION_ERROR
var VAST_PARSE_FAILED
var VAST_VERSION_MISMATCH
var VAST_WRAPPER_DEPTH_EXCEEDED
var VERSION_UNSUPPORTED
class EventType (*args, **kwds)
Expand source code
class EventType(StrEnum):
    page_view = 'page_view'
    view_content = 'view_content'
    select_content = 'select_content'
    select_item = 'select_item'
    search = 'search'
    share = 'share'
    add_to_cart = 'add_to_cart'
    remove_from_cart = 'remove_from_cart'
    viewed_cart = 'viewed_cart'
    add_to_wishlist = 'add_to_wishlist'
    initiate_checkout = 'initiate_checkout'
    add_payment_info = 'add_payment_info'
    purchase = 'purchase'
    refund = 'refund'
    lead = 'lead'
    qualify_lead = 'qualify_lead'
    close_convert_lead = 'close_convert_lead'
    disqualify_lead = 'disqualify_lead'
    complete_registration = 'complete_registration'
    subscribe = 'subscribe'
    follow = 'follow'
    content_view = 'content_view'
    watch_milestone = 'watch_milestone'
    start_trial = 'start_trial'
    app_install = 'app_install'
    app_launch = 'app_launch'
    contact = 'contact'
    schedule = 'schedule'
    donate = 'donate'
    submit_application = 'submit_application'
    custom = 'custom'

Enum where members are also (and must be) strings

Ancestors

  • enum.StrEnum
  • builtins.str
  • enum.ReprEnum
  • enum.Enum

Class variables

var add_payment_info
var add_to_cart
var add_to_wishlist
var app_install
var app_launch
var close_convert_lead
var complete_registration
var contact
var content_view
var custom
var disqualify_lead
var donate
var follow
var initiate_checkout
var lead
var page_view
var purchase
var qualify_lead
var refund
var remove_from_cart
var schedule
var search
var select_content
var select_item
var share
var start_trial
var submit_application
var subscribe
var view_content
var viewed_cart
var watch_milestone
class ExtensionObject (**data: Any)
Expand source code
class ExtensionObject(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var model_config

Inherited members

class FeatureResolver (capabilities: GetAdcpCapabilitiesResponse)
Expand source code
class FeatureResolver:
    """Resolves feature support from a GetAdcpCapabilitiesResponse.

    Supports multiple feature namespaces:

    - Protocol support: ``"media_buy"`` checks ``supported_protocols``
    - Extension support: ``"ext:scope3"`` checks ``extensions_supported``
    - Targeting: ``"targeting.geo_countries"`` checks
      ``media_buy.execution.targeting``
    - Media buy features: ``"inline_creative_management"`` checks
      ``media_buy.features``
    - Signals features: ``"catalog_signals"`` checks
      ``signals.features``
    """

    def __init__(self, capabilities: GetAdcpCapabilitiesResponse) -> None:
        self._caps = capabilities

        # Pre-compute the set of valid protocol names so supports() doesn't
        # need a runtime import on every call.
        from adcp.types.generated_poc.protocol.get_adcp_capabilities_response import (
            SupportedProtocol,
        )

        self._valid_protocols = {p.value for p in SupportedProtocol}
        self._declared_protocols = {p.value for p in capabilities.supported_protocols}

    @property
    def capabilities(self) -> GetAdcpCapabilitiesResponse:
        return self._caps

    def supports_v3(self) -> bool:
        """Check if the seller supports ADCP v3.

        Returns:
            True if major_versions includes 3.
        """
        for v in self._caps.adcp.major_versions:
            if (v.root if hasattr(v, "root") else v) == 3:
                return True
        return False

    def supports(self, feature: str) -> bool:
        """Check if a feature is supported."""
        caps = self._caps

        # Extension check: "ext:scope3"
        if feature.startswith("ext:"):
            ext_name = feature[4:]
            if caps.extensions_supported is None:
                return False
            return any(item.root == ext_name for item in caps.extensions_supported)

        # Targeting check: "targeting.geo_countries"
        if feature.startswith("targeting."):
            attr_name = feature[len("targeting.") :]
            if caps.media_buy is None or caps.media_buy.execution is None:
                return False
            targeting = caps.media_buy.execution.targeting
            if targeting is None:
                return False
            if attr_name not in type(targeting).model_fields:
                return False
            val = getattr(targeting, attr_name, None)
            # For bool fields, check truthiness. For object fields (like geo_metros),
            # presence means supported.
            return val is not None and val is not False

        # Protocol check: if the string is a known protocol name, resolve it
        # against supported_protocols and stop — don't fall through to features.
        if feature in self._declared_protocols:
            return True
        if feature in self._valid_protocols:
            return False

        # Media buy features check
        if caps.media_buy is not None and caps.media_buy.features is not None:
            if feature in type(caps.media_buy.features).model_fields:
                val = getattr(caps.media_buy.features, feature, None)
                if val is True:
                    return True

        # Signals features check
        if caps.signals is not None and caps.signals.features is not None:
            if feature in type(caps.signals.features).model_fields:
                val = getattr(caps.signals.features, feature, None)
                if val is True:
                    return True

        return False

    def require(
        self,
        *features: str,
        agent_id: str | None = None,
        agent_uri: str | None = None,
    ) -> None:
        """Assert that all listed features are supported.

        Args:
            *features: Feature identifiers to require.
            agent_id: Optional agent ID for error context.
            agent_uri: Optional agent URI for error context.

        Raises:
            ADCPFeatureUnsupportedError: If any features are not supported.
        """
        unsupported = [f for f in features if not self.supports(f)]
        if not unsupported:
            return

        declared = self.get_declared_features()

        raise ADCPFeatureUnsupportedError(
            unsupported_features=unsupported,
            declared_features=declared,
            agent_id=agent_id,
            agent_uri=agent_uri,
        )

    def get_declared_features(self) -> list[str]:
        """Collect all features the response declares as supported."""
        caps = self._caps
        declared: list[str] = []

        # Supported protocols
        for p in caps.supported_protocols:
            declared.append(p.value)

        # Media buy features
        if caps.media_buy is not None and caps.media_buy.features is not None:
            for field_name in type(caps.media_buy.features).model_fields:
                if getattr(caps.media_buy.features, field_name, None) is True:
                    declared.append(field_name)

        # Signals features
        if caps.signals is not None and caps.signals.features is not None:
            for field_name in type(caps.signals.features).model_fields:
                if getattr(caps.signals.features, field_name, None) is True:
                    declared.append(field_name)

        # Targeting features
        if caps.media_buy is not None and caps.media_buy.execution is not None:
            targeting = caps.media_buy.execution.targeting
            if targeting is not None:
                for field_name in type(targeting).model_fields:
                    val = getattr(targeting, field_name, None)
                    if val is not None and val is not False:
                        declared.append(f"targeting.{field_name}")

        # Extensions
        if caps.extensions_supported is not None:
            for item in caps.extensions_supported:
                declared.append(f"ext:{item.root}")

        return declared

Resolves feature support from a GetAdcpCapabilitiesResponse.

Supports multiple feature namespaces:

  • Protocol support: "media_buy" checks supported_protocols
  • Extension support: "ext:scope3" checks extensions_supported
  • Targeting: "targeting.geo_countries" checks media_buy.execution.targeting
  • Media buy features: "inline_creative_management" checks media_buy.features
  • Signals features: "catalog_signals" checks signals.features

Instance variables

prop capabilities : GetAdcpCapabilitiesResponse
Expand source code
@property
def capabilities(self) -> GetAdcpCapabilitiesResponse:
    return self._caps

Methods

def get_declared_features(self) ‑> list[str]
Expand source code
def get_declared_features(self) -> list[str]:
    """Collect all features the response declares as supported."""
    caps = self._caps
    declared: list[str] = []

    # Supported protocols
    for p in caps.supported_protocols:
        declared.append(p.value)

    # Media buy features
    if caps.media_buy is not None and caps.media_buy.features is not None:
        for field_name in type(caps.media_buy.features).model_fields:
            if getattr(caps.media_buy.features, field_name, None) is True:
                declared.append(field_name)

    # Signals features
    if caps.signals is not None and caps.signals.features is not None:
        for field_name in type(caps.signals.features).model_fields:
            if getattr(caps.signals.features, field_name, None) is True:
                declared.append(field_name)

    # Targeting features
    if caps.media_buy is not None and caps.media_buy.execution is not None:
        targeting = caps.media_buy.execution.targeting
        if targeting is not None:
            for field_name in type(targeting).model_fields:
                val = getattr(targeting, field_name, None)
                if val is not None and val is not False:
                    declared.append(f"targeting.{field_name}")

    # Extensions
    if caps.extensions_supported is not None:
        for item in caps.extensions_supported:
            declared.append(f"ext:{item.root}")

    return declared

Collect all features the response declares as supported.

def require(self, *features: str, agent_id: str | None = None, agent_uri: str | None = None) ‑> None
Expand source code
def require(
    self,
    *features: str,
    agent_id: str | None = None,
    agent_uri: str | None = None,
) -> None:
    """Assert that all listed features are supported.

    Args:
        *features: Feature identifiers to require.
        agent_id: Optional agent ID for error context.
        agent_uri: Optional agent URI for error context.

    Raises:
        ADCPFeatureUnsupportedError: If any features are not supported.
    """
    unsupported = [f for f in features if not self.supports(f)]
    if not unsupported:
        return

    declared = self.get_declared_features()

    raise ADCPFeatureUnsupportedError(
        unsupported_features=unsupported,
        declared_features=declared,
        agent_id=agent_id,
        agent_uri=agent_uri,
    )

Assert that all listed features are supported.

Args
-----=
*features
Feature identifiers to require.
agent_id
Optional agent ID for error context.
agent_uri
Optional agent URI for error context.
Raises
-----=
ADCPFeatureUnsupportedError
If any features are not supported.
def supports(self, feature: str) ‑> bool
Expand source code
def supports(self, feature: str) -> bool:
    """Check if a feature is supported."""
    caps = self._caps

    # Extension check: "ext:scope3"
    if feature.startswith("ext:"):
        ext_name = feature[4:]
        if caps.extensions_supported is None:
            return False
        return any(item.root == ext_name for item in caps.extensions_supported)

    # Targeting check: "targeting.geo_countries"
    if feature.startswith("targeting."):
        attr_name = feature[len("targeting.") :]
        if caps.media_buy is None or caps.media_buy.execution is None:
            return False
        targeting = caps.media_buy.execution.targeting
        if targeting is None:
            return False
        if attr_name not in type(targeting).model_fields:
            return False
        val = getattr(targeting, attr_name, None)
        # For bool fields, check truthiness. For object fields (like geo_metros),
        # presence means supported.
        return val is not None and val is not False

    # Protocol check: if the string is a known protocol name, resolve it
    # against supported_protocols and stop — don't fall through to features.
    if feature in self._declared_protocols:
        return True
    if feature in self._valid_protocols:
        return False

    # Media buy features check
    if caps.media_buy is not None and caps.media_buy.features is not None:
        if feature in type(caps.media_buy.features).model_fields:
            val = getattr(caps.media_buy.features, feature, None)
            if val is True:
                return True

    # Signals features check
    if caps.signals is not None and caps.signals.features is not None:
        if feature in type(caps.signals.features).model_fields:
            val = getattr(caps.signals.features, feature, None)
            if val is True:
                return True

    return False

Check if a feature is supported.

def supports_v3(self) ‑> bool
Expand source code
def supports_v3(self) -> bool:
    """Check if the seller supports ADCP v3.

    Returns:
        True if major_versions includes 3.
    """
    for v in self._caps.adcp.major_versions:
        if (v.root if hasattr(v, "root") else v) == 3:
            return True
    return False

Check if the seller supports ADCP v3.

Returns -----= True if major_versions includes 3.

class FederatedAgentWithDetails (**data: Any)
Expand source code
class FederatedAgentWithDetails(RegistryBaseModel):
    url: str
    name: str
    type: AgentType
    protocol: AgentProtocol | None = None
    description: str | None = None
    mcp_endpoint: str | None = None
    contact: AgentDetailedContact | None = None
    added_date: str | None = None
    member: Annotated[
        AgentMember | None,
        Field(
            description="AAO member that owns this agent record. The registry contains only agents that members have explicitly enrolled on their member profile."
        ),
    ] = None
    health: AgentHealth | None = None
    stats: AgentStats | None = None
    capabilities: AgentCapabilities | None = None
    compliance: AgentCompliance | None = None
    publisher_domains: list[str] | None = None
    property_summary: PropertySummary | None = None

Base model for registry API types.

Uses extra='allow' so that new fields from the registry API are preserved rather than dropped. This differs from AdCPBaseModel which defaults to extra='ignore' for protocol types.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var added_date : str | None
var capabilitiesAgentCapabilities | None
var complianceAgentCompliance | None
var contactAgentDetailedContact | None
var description : str | None
var healthAgentHealth | None
var mcp_endpoint : str | None
var memberAgentMember | None
var model_config
var name : str
var property_summaryPropertySummary | None
var protocolAgentProtocol | None
var publisher_domains : list[str] | None
var statsAgentStats | None
var typeAgentType
var url : str
class FederatedPublisher (**data: Any)
Expand source code
class FederatedPublisher(RegistryBaseModel):
    domain: str
    member: AgentMember | None = None
    agent_count: int | None = None
    last_validated: str | None = None
    has_valid_adagents: bool | None = None

Base model for registry API types.

Uses extra='allow' so that new fields from the registry API are preserved rather than dropped. This differs from AdCPBaseModel which defaults to extra='ignore' for protocol types.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var agent_count : int | None
var domain : str
var has_valid_adagents : bool | None
var last_validated : str | None
var memberAgentMember | None
var model_config
class FeedEvent (**data: Any)
Expand source code
class FeedEvent(RegistryBaseModel):
    event_id: str
    event_type: str
    entity_type: str
    entity_id: str
    payload: dict[str, Any]
    actor: str
    created_at: str

Base model for registry API types.

Uses extra='allow' so that new fields from the registry API are preserved rather than dropped. This differs from AdCPBaseModel which defaults to extra='ignore' for protocol types.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var actor : str
var created_at : str
var entity_id : str
var entity_type : str
var event_id : str
var event_type : str
var model_config
var payload : dict[str, typing.Any]
class FeedFormat (*args, **kwds)
Expand source code
class FeedFormat(StrEnum):
    google_merchant_center = 'google_merchant_center'
    facebook_catalog = 'facebook_catalog'
    shopify = 'shopify'
    linkedin_jobs = 'linkedin_jobs'
    tiktok_shop = 'tiktok_shop'
    pinterest_catalog = 'pinterest_catalog'
    openai_product_feed = 'openai_product_feed'
    custom = 'custom'

Enum where members are also (and must be) strings

Ancestors

  • enum.StrEnum
  • builtins.str
  • enum.ReprEnum
  • enum.Enum

Class variables

var custom
var facebook_catalog
var google_merchant_center
var linkedin_jobs
var openai_product_feed
var pinterest_catalog
var shopify
var tiktok_shop
class FeedMirror (client: FeedMirrorClient,
*,
account: AccountReference | None = None,
on_event: EventHandler | None = None,
state_store: FeedStateStore | None = None,
page_limit: int = 100)
Expand source code
class FeedMirror:
    """In-memory mirror of an agent's wholesale product and signal feeds.

    Args:
        client: An :class:`adcp.ADCPClient` (or any object satisfying
            :class:`FeedMirrorClient`) bound to the target agent.
        account: Optional account scope for wholesale reads. Wholesale-feed
            webhooks are account-anchored, so pass the same account used to
            register ``notification_configs[]`` so repair reads reconcile the
            correct public or account overlay.
        on_event: Optional callback invoked after each webhook event mutates
            the mirror. Receives the applied :class:`WholesaleFeedEvent`.
        state_store: Optional :class:`FeedStateStore` for persisting cached
            feed-version tokens across process restarts.
        page_limit: Page size for wholesale enumeration (max 100 per spec).
    """

    def __init__(
        self,
        client: FeedMirrorClient,
        *,
        account: AccountReference | None = None,
        on_event: EventHandler | None = None,
        state_store: FeedStateStore | None = None,
        page_limit: int = _DEFAULT_PAGE_LIMIT,
    ) -> None:
        self._client = client
        self._account = account
        self._on_event = on_event
        self._state_store = state_store
        self._page_limit = page_limit

        self._products: dict[str, Product] = {}
        self._signals: dict[str, Signal] = {}
        self._product_state = FeedState()
        self._signal_state = FeedState()

    # ------------------------------------------------------------------
    # Read-only views
    # ------------------------------------------------------------------

    @property
    def products(self) -> dict[str, Product]:
        """Live view of the product index keyed by ``product_id``."""
        return self._products

    @property
    def signals(self) -> dict[str, Signal]:
        """Live view of the signal index keyed by ``signal_agent_segment_id``."""
        return self._signals

    @property
    def product_state(self) -> FeedState:
        """Cached version tokens for the wholesale product feed."""
        return self._product_state

    @property
    def signal_state(self) -> FeedState:
        """Cached version tokens for the wholesale signal feed."""
        return self._signal_state

    def get_product(self, product_id: str) -> Product | None:
        """Return a mirrored product by id, or ``None``."""
        return self._products.get(product_id)

    def get_signal(self, signal_agent_segment_id: str) -> Signal | None:
        """Return a mirrored signal by id, or ``None``."""
        return self._signals.get(signal_agent_segment_id)

    # ------------------------------------------------------------------
    # Bootstrap / refresh
    # ------------------------------------------------------------------

    async def bootstrap(self, entities: FeedEntity | Literal["all"] = "all") -> RefreshResult:
        """Initial full load of the wholesale feed(s).

        Restores any persisted :class:`FeedState` from the configured
        ``state_store`` first, so a bootstrap after a restart presents the
        cached version on the conditional read and short-circuits when the
        seller has nothing new.

        Args:
            entities: ``"all"`` (default), ``"product"``, or ``"signal"`` to
                bootstrap a single feed.
        """
        if self._state_store is not None:
            await self._restore_state(entities)
        return await self._fetch(entities)

    async def refresh(self, entities: FeedEntity | Literal["all"] = "all") -> RefreshResult:
        """Conditional re-read of the wholesale feed(s).

        Presents the cached ``if_wholesale_feed_version`` (and
        ``if_pricing_version`` when known). When the seller returns
        ``unchanged: true`` the replica is left untouched and the result's
        ``*_unchanged`` flag is set.
        """
        return await self._fetch(entities)

    async def _fetch(self, entities: FeedEntity | Literal["all"]) -> RefreshResult:
        result = RefreshResult()
        do_products = entities in ("all", "product")
        do_signals = entities in ("all", "signal")

        if do_products:
            meta = await self._fetch_products()
            self._commit("product", meta)
            result.products_unchanged = meta.unchanged
        else:
            # Treat a feed we didn't fetch as unchanged so RefreshResult.unchanged
            # reflects only the feeds the caller asked about.
            result.products_unchanged = True

        if do_signals:
            meta = await self._fetch_signals()
            self._commit("signal", meta)
            result.signals_unchanged = meta.unchanged
        else:
            result.signals_unchanged = True

        result.product_count = len(self._products)
        result.signal_count = len(self._signals)
        return result

    async def _fetch_products(self) -> _FeedMetadata:
        state = self._product_state
        meta = _FeedMetadata(
            wholesale_feed_version=state.wholesale_feed_version,
            pricing_version=state.pricing_version,
            cache_scope=state.cache_scope,
        )
        cursor: str | None = None
        first_page = True
        while True:
            request = self._build_products_request(cursor, first_page, state)
            task = await self._client.get_products(request)
            body = self._require_body(task, "get_products")
            if body.unchanged:
                self._merge_metadata(meta, body)
                meta.unchanged = True
                return meta
            for product in body.products or []:
                meta.items[product.product_id] = product
            self._merge_metadata(meta, body)
            cursor = self._next_cursor(body)
            first_page = False
            if cursor is None:
                return meta

    async def _fetch_signals(self) -> _FeedMetadata:
        state = self._signal_state
        meta = _FeedMetadata(
            wholesale_feed_version=state.wholesale_feed_version,
            pricing_version=state.pricing_version,
            cache_scope=state.cache_scope,
        )
        cursor: str | None = None
        first_page = True
        while True:
            request = self._build_signals_request(cursor, first_page, state)
            task = await self._client.get_signals(request)
            body = self._require_body(task, "get_signals")
            if body.unchanged:
                self._merge_metadata(meta, body)
                meta.unchanged = True
                return meta
            for signal in body.signals or []:
                meta.items[signal.signal_agent_segment_id] = signal
            self._merge_metadata(meta, body)
            cursor = self._next_cursor(body)
            first_page = False
            if cursor is None:
                return meta

    def _build_products_request(
        self, cursor: str | None, first_page: bool, state: FeedState
    ) -> GetProductsRequest:
        kwargs: dict[str, Any] = {
            "buying_mode": "wholesale",
            "pagination": PaginationRequest(max_results=self._page_limit, cursor=cursor),
        }
        if self._account is not None:
            kwargs["account"] = self._account
        # Conditional fetch on the first page only — pagination.cursor is not
        # part of the version scoping tuple, so a mid-walk page must not carry
        # the if_* tokens.
        if first_page and state.wholesale_feed_version is not None:
            kwargs["if_wholesale_feed_version"] = state.wholesale_feed_version
            if state.pricing_version is not None:
                kwargs["if_pricing_version"] = state.pricing_version
        return GetProductsRequest(**kwargs)

    def _build_signals_request(
        self, cursor: str | None, first_page: bool, state: FeedState
    ) -> GetSignalsRequest:
        kwargs: dict[str, Any] = {
            "discovery_mode": "wholesale",
            "pagination": PaginationRequest(max_results=self._page_limit, cursor=cursor),
        }
        if self._account is not None:
            kwargs["account"] = self._account
        if first_page and state.wholesale_feed_version is not None:
            kwargs["if_wholesale_feed_version"] = state.wholesale_feed_version
            if state.pricing_version is not None:
                kwargs["if_pricing_version"] = state.pricing_version
        return GetSignalsRequest(**kwargs)

    @staticmethod
    def _require_body(task: TaskResult[Any], operation: str) -> Any:
        if not task.success or task.data is None:
            raise FeedMirrorError(
                f"{operation} did not return a successful wholesale response: "
                f"{task.error or task.message or task.status}"
            )
        return task.data

    @staticmethod
    def _merge_metadata(meta: _FeedMetadata, body: Any) -> None:
        if body.wholesale_feed_version is not None:
            meta.wholesale_feed_version = body.wholesale_feed_version
        if body.pricing_version is not None:
            meta.pricing_version = body.pricing_version
        if body.cache_scope is not None:
            meta.cache_scope = _scope_str(body.cache_scope)

    @staticmethod
    def _next_cursor(body: Any) -> str | None:
        pagination = body.pagination
        if pagination is not None and pagination.has_more:
            return cast("str | None", pagination.cursor)
        return None

    def _commit(self, entity: FeedEntity, meta: _FeedMetadata) -> None:
        state = self._product_state if entity == "product" else self._signal_state
        state.wholesale_feed_version = meta.wholesale_feed_version
        state.pricing_version = meta.pricing_version
        state.cache_scope = meta.cache_scope
        if not meta.unchanged:
            # Atomic swap — only replace the live index on a fresh fetch so an
            # unchanged short-circuit never wipes the replica.
            if entity == "product":
                self._products = meta.items
            else:
                self._signals = meta.items

    async def _restore_state(self, entities: FeedEntity | Literal["all"]) -> None:
        assert self._state_store is not None
        if entities in ("all", "product"):
            restored = await self._state_store.load("product")
            if restored is not None:
                self._product_state = restored
        if entities in ("all", "signal"):
            restored = await self._state_store.load("signal")
            if restored is not None:
                self._signal_state = restored

    async def _persist_state(self, entity: FeedEntity) -> None:
        if self._state_store is None:
            return
        state = self._product_state if entity == "product" else self._signal_state
        await self._state_store.save(entity, state)

    # ------------------------------------------------------------------
    # Incremental webhook application
    # ------------------------------------------------------------------

    async def apply_webhook(self, webhook: WholesaleFeedWebhook) -> RefreshResult | None:
        """Apply one wholesale-feed webhook to the local mirror.

        Call this from your HTTP webhook receiver after signature/auth
        validation. ``product.*`` / ``signal.*`` events mutate the index in
        place (events are denormalized — no follow-up read needed) and update
        the cached feed version for the affected feed. A
        ``wholesale_feed.bulk_change`` event re-bootstraps only the feed named
        by ``affected_entity_type`` and returns the :class:`RefreshResult`.

        Raises:
            FeedMirrorError: When the webhook envelope is internally
                inconsistent (``notification_type`` / ``notification_id`` do
                not match the embedded event).
        """
        event = webhook.event
        if webhook.notification_type != event.event_type:
            raise FeedMirrorError("webhook notification_type does not match event.event_type")
        if str(webhook.notification_id) != str(event.event_id):
            raise FeedMirrorError("webhook notification_id does not match event.event_id")

        if event.event_type == "wholesale_feed.bulk_change":
            affected = self._bulk_change_entity(event)
            result = await self.refresh(affected)
            self._dispatch(event)
            return result

        self._apply_event(event)
        self._remember_webhook_version(webhook)
        self._dispatch(event)
        await self._persist_state(self._event_entity(event))
        return None

    @staticmethod
    def _bulk_change_entity(event: WholesaleFeedEvent) -> FeedEntity:
        affected: Any = getattr(event.payload, "affected_entity_type", None)
        value = affected.value if hasattr(affected, "value") else affected
        if value == "product":
            return "product"
        if value == "signal":
            return "signal"
        raise FeedMirrorError(
            "wholesale_feed.bulk_change payload missing required affected_entity_type"
        )

    @staticmethod
    def _event_entity(event: WholesaleFeedEvent) -> FeedEntity:
        return "product" if str(event.event_type).startswith("product.") else "signal"

    def _apply_event(self, event: WholesaleFeedEvent) -> None:
        event_type = str(event.event_type)
        payload = event.payload
        if event_type in ("product.created", "product.updated"):
            product = getattr(payload, "product", None)
            if product is not None:
                self._products[product.product_id] = product
        elif event_type == "product.priced":
            existing = self._products.get(payload.product_id)
            if existing is not None:
                self._products[payload.product_id] = existing.model_copy(
                    update={"pricing_options": payload.pricing_options}
                )
        elif event_type == "product.removed":
            self._products.pop(payload.product_id, None)
        elif event_type in ("signal.created", "signal.updated"):
            signal = getattr(payload, "signal", None)
            if signal is not None:
                self._signals[signal.signal_agent_segment_id] = signal
        elif event_type == "signal.priced":
            existing_signal = self._signals.get(payload.signal_agent_segment_id)
            if existing_signal is not None:
                self._signals[payload.signal_agent_segment_id] = existing_signal.model_copy(
                    update={"pricing_options": payload.pricing_options}
                )
        elif event_type == "signal.removed":
            self._signals.pop(payload.signal_agent_segment_id, None)

    def _remember_webhook_version(self, webhook: WholesaleFeedWebhook) -> None:
        entity = self._event_entity(webhook.event)
        state = self._product_state if entity == "product" else self._signal_state
        state.wholesale_feed_version = webhook.wholesale_feed_version
        state.cache_scope = _scope_str(webhook.cache_scope)

    def _dispatch(self, event: WholesaleFeedEvent) -> None:
        if self._on_event is None:
            return
        try:
            self._on_event(event)
        except Exception:
            logger.exception("FeedMirror on_event handler raised")

In-memory mirror of an agent's wholesale product and signal feeds.

Args
-----=
client
An :class:ADCPClient (or any object satisfying :class:FeedMirrorClient) bound to the target agent.
account
Optional account scope for wholesale reads. Wholesale-feed webhooks are account-anchored, so pass the same account used to register notification_configs[] so repair reads reconcile the correct public or account overlay.
on_event
Optional callback invoked after each webhook event mutates the mirror. Receives the applied :class:WholesaleFeedEvent.
state_store
Optional :class:FeedStateStore for persisting cached feed-version tokens across process restarts.
page_limit
Page size for wholesale enumeration (max 100 per spec).

Instance variables

prop product_stateFeedState
Expand source code
@property
def product_state(self) -> FeedState:
    """Cached version tokens for the wholesale product feed."""
    return self._product_state

Cached version tokens for the wholesale product feed.

prop products : dict[str, Product]
Expand source code
@property
def products(self) -> dict[str, Product]:
    """Live view of the product index keyed by ``product_id``."""
    return self._products

Live view of the product index keyed by product_id.

prop signal_stateFeedState
Expand source code
@property
def signal_state(self) -> FeedState:
    """Cached version tokens for the wholesale signal feed."""
    return self._signal_state

Cached version tokens for the wholesale signal feed.

prop signals : dict[str, Signal]
Expand source code
@property
def signals(self) -> dict[str, Signal]:
    """Live view of the signal index keyed by ``signal_agent_segment_id``."""
    return self._signals

Live view of the signal index keyed by signal_agent_segment_id.

Methods

async def apply_webhook(self,
webhook: WholesaleFeedWebhook) ‑> RefreshResult | None
Expand source code
async def apply_webhook(self, webhook: WholesaleFeedWebhook) -> RefreshResult | None:
    """Apply one wholesale-feed webhook to the local mirror.

    Call this from your HTTP webhook receiver after signature/auth
    validation. ``product.*`` / ``signal.*`` events mutate the index in
    place (events are denormalized — no follow-up read needed) and update
    the cached feed version for the affected feed. A
    ``wholesale_feed.bulk_change`` event re-bootstraps only the feed named
    by ``affected_entity_type`` and returns the :class:`RefreshResult`.

    Raises:
        FeedMirrorError: When the webhook envelope is internally
            inconsistent (``notification_type`` / ``notification_id`` do
            not match the embedded event).
    """
    event = webhook.event
    if webhook.notification_type != event.event_type:
        raise FeedMirrorError("webhook notification_type does not match event.event_type")
    if str(webhook.notification_id) != str(event.event_id):
        raise FeedMirrorError("webhook notification_id does not match event.event_id")

    if event.event_type == "wholesale_feed.bulk_change":
        affected = self._bulk_change_entity(event)
        result = await self.refresh(affected)
        self._dispatch(event)
        return result

    self._apply_event(event)
    self._remember_webhook_version(webhook)
    self._dispatch(event)
    await self._persist_state(self._event_entity(event))
    return None

Apply one wholesale-feed webhook to the local mirror.

Call this from your HTTP webhook receiver after signature/auth validation. product.* / signal.* events mutate the index in place (events are denormalized — no follow-up read needed) and update the cached feed version for the affected feed. A wholesale_feed.bulk_change event re-bootstraps only the feed named by affected_entity_type and returns the :class:RefreshResult.

Raises
-----=
FeedMirrorError
When the webhook envelope is internally inconsistent (notification_type / notification_id do not match the embedded event).
async def bootstrap(self, entities: "FeedEntity | Literal['all']" = 'all') ‑> RefreshResult
Expand source code
async def bootstrap(self, entities: FeedEntity | Literal["all"] = "all") -> RefreshResult:
    """Initial full load of the wholesale feed(s).

    Restores any persisted :class:`FeedState` from the configured
    ``state_store`` first, so a bootstrap after a restart presents the
    cached version on the conditional read and short-circuits when the
    seller has nothing new.

    Args:
        entities: ``"all"`` (default), ``"product"``, or ``"signal"`` to
            bootstrap a single feed.
    """
    if self._state_store is not None:
        await self._restore_state(entities)
    return await self._fetch(entities)

Initial full load of the wholesale feed(s).

Restores any persisted :class:FeedState from the configured state_store first, so a bootstrap after a restart presents the cached version on the conditional read and short-circuits when the seller has nothing new.

Args
-----=
entities
"all" (default), "product", or "signal" to bootstrap a single feed.
def get_product(self, product_id: str) ‑> Product | None
Expand source code
def get_product(self, product_id: str) -> Product | None:
    """Return a mirrored product by id, or ``None``."""
    return self._products.get(product_id)

Return a mirrored product by id, or None.

def get_signal(self, signal_agent_segment_id: str) ‑> adcp.types.generated_poc.core.wholesale_feed_event.Signal | None
Expand source code
def get_signal(self, signal_agent_segment_id: str) -> Signal | None:
    """Return a mirrored signal by id, or ``None``."""
    return self._signals.get(signal_agent_segment_id)

Return a mirrored signal by id, or None.

async def refresh(self, entities: "FeedEntity | Literal['all']" = 'all') ‑> RefreshResult
Expand source code
async def refresh(self, entities: FeedEntity | Literal["all"] = "all") -> RefreshResult:
    """Conditional re-read of the wholesale feed(s).

    Presents the cached ``if_wholesale_feed_version`` (and
    ``if_pricing_version`` when known). When the seller returns
    ``unchanged: true`` the replica is left untouched and the result's
    ``*_unchanged`` flag is set.
    """
    return await self._fetch(entities)

Conditional re-read of the wholesale feed(s).

Presents the cached if_wholesale_feed_version (and if_pricing_version when known). When the seller returns unchanged: true the replica is left untouched and the result's *_unchanged flag is set.

class FeedMirrorClient (*args, **kwargs)
Expand source code
@runtime_checkable
class FeedMirrorClient(Protocol):
    """The subset of :class:`adcp.ADCPClient` a :class:`FeedMirror` calls.

    Declared as a Protocol so tests can inject a minimal stub and so the
    mirror does not import the concrete client (avoiding a cycle).
    """

    async def get_products(
        self, request: GetProductsRequest
    ) -> TaskResult[GetProductsResponse]: ...

    async def get_signals(self, request: GetSignalsRequest) -> TaskResult[GetSignalsResponse]: ...

The subset of :class:ADCPClient a :class:FeedMirror calls.

Declared as a Protocol so tests can inject a minimal stub and so the mirror does not import the concrete client (avoiding a cycle).

Ancestors

  • typing.Protocol
  • typing.Generic

Methods

async def get_products(self,
request: GetProductsRequest) ‑> TaskResult[GetProductsResponse]
Expand source code
async def get_products(
    self, request: GetProductsRequest
) -> TaskResult[GetProductsResponse]: ...
async def get_signals(self,
request: GetSignalsRequest) ‑> TaskResult[GetSignalsResponse]
Expand source code
async def get_signals(self, request: GetSignalsRequest) -> TaskResult[GetSignalsResponse]: ...
class FeedMirrorError (*args, **kwargs)
Expand source code
class FeedMirrorError(Exception):
    """Raised when a wholesale-feed read fails or a webhook is inconsistent."""

Raised when a wholesale-feed read fails or a webhook is inconsistent.

Ancestors

  • builtins.Exception
  • builtins.BaseException
class FeedPage (**data: Any)
Expand source code
class FeedPage(RegistryBaseModel):
    events: list[FeedEvent]
    cursor: str | None
    has_more: bool

Base model for registry API types.

Uses extra='allow' so that new fields from the registry API are preserved rather than dropped. This differs from AdCPBaseModel which defaults to extra='ignore' for protocol types.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var cursor : str | None
var events : list[FeedEvent]
var has_more : bool
var model_config
class FeedState (wholesale_feed_version: str | None = None,
pricing_version: str | None = None,
cache_scope: "Literal['public', 'account']" = 'public')
Expand source code
@dataclass
class FeedState:
    """Cached version tokens for one wholesale feed.

    The tokens are opaque per the spec — the mirror never inspects or orders
    them, it only echoes the cached ``wholesale_feed_version`` (and
    ``pricing_version`` when present) back on the next conditional read.
    """

    wholesale_feed_version: str | None = None
    pricing_version: str | None = None
    cache_scope: Literal["public", "account"] = "public"

Cached version tokens for one wholesale feed.

The tokens are opaque per the spec — the mirror never inspects or orders them, it only echoes the cached wholesale_feed_version (and pricing_version when present) back on the next conditional read.

Instance variables

var cache_scope : Literal['public', 'account']
var pricing_version : str | None
var wholesale_feed_version : str | None
class FeedStateStore (*args, **kwargs)
Expand source code
@runtime_checkable
class FeedStateStore(Protocol):
    """Optional persistence hook for cached feed-version state.

    Lets adopters survive a process restart without re-bootstrapping from
    scratch: persist :class:`FeedState` per feed on save, restore on load.
    All methods are async to allow database-backed implementations.
    """

    async def load(self, entity: FeedEntity) -> FeedState | None:
        """Return the persisted state for a feed, or ``None`` if absent."""
        ...

    async def save(self, entity: FeedEntity, state: FeedState) -> None:
        """Persist the state for a feed."""
        ...

Optional persistence hook for cached feed-version state.

Lets adopters survive a process restart without re-bootstrapping from scratch: persist :class:FeedState per feed on save, restore on load. All methods are async to allow database-backed implementations.

Ancestors

  • typing.Protocol
  • typing.Generic

Methods

async def load(self, entity: FeedEntity) ‑> FeedState | None
Expand source code
async def load(self, entity: FeedEntity) -> FeedState | None:
    """Return the persisted state for a feed, or ``None`` if absent."""
    ...

Return the persisted state for a feed, or None if absent.

async def save(self,
entity: FeedEntity,
state: FeedState) ‑> None
Expand source code
async def save(self, entity: FeedEntity, state: FeedState) -> None:
    """Persist the state for a feed."""
    ...

Persist the state for a feed.

class FileCursorStore (path: str | Path = '.adcp-sync-cursor.json')
Expand source code
class FileCursorStore:
    """Default cursor store using a local JSON file.

    Args:
        path: Path to the cursor file. Defaults to .adcp-sync-cursor.json
    """

    def __init__(self, path: str | Path = ".adcp-sync-cursor.json") -> None:
        self._path = Path(path)

    async def load(self) -> str | None:
        try:
            data = json.loads(self._path.read_text())
            return data.get("cursor")  # type: ignore[no-any-return]
        except (FileNotFoundError, json.JSONDecodeError, KeyError):
            return None

    async def save(self, cursor: str) -> None:
        temp = self._path.with_suffix(".tmp")
        temp.write_text(json.dumps({"cursor": cursor}))
        temp.replace(self._path)  # Atomic rename

Default cursor store using a local JSON file.

Args
-----=
path
Path to the cursor file. Defaults to .adcp-sync-cursor.json

Methods

async def load(self) ‑> str | None
Expand source code
async def load(self) -> str | None:
    try:
        data = json.loads(self._path.read_text())
        return data.get("cursor")  # type: ignore[no-any-return]
    except (FileNotFoundError, json.JSONDecodeError, KeyError):
        return None
async def save(self, cursor: str) ‑> None
Expand source code
async def save(self, cursor: str) -> None:
    temp = self._path.with_suffix(".tmp")
    temp.write_text(json.dumps({"cursor": cursor}))
    temp.replace(self._path)  # Atomic rename
class FlatRatePricingOption (**data: Any)
Expand source code
class FlatRatePricingOption(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    pricing_option_id: Annotated[
        str, Field(description='Unique identifier for this pricing option within the product')
    ]
    pricing_model: Annotated[
        Literal['flat_rate'], Field(description='Fixed cost regardless of delivery volume')
    ] = 'flat_rate'
    currency: Annotated[
        str,
        Field(
            description='ISO 4217 currency code',
            examples=['USD', 'EUR', 'GBP', 'JPY'],
            pattern='^[A-Z]{3}$',
        ),
    ]
    fixed_price: Annotated[
        float | None,
        Field(
            description='Flat rate cost. If present, this is fixed pricing. If absent, auction-based.',
            ge=0.0,
        ),
    ] = None
    floor_price: Annotated[
        float | None,
        Field(
            description='Minimum acceptable bid for auction pricing (mutually exclusive with fixed_price). Bids below this value will be rejected.',
            ge=0.0,
        ),
    ] = None
    price_guidance: Annotated[
        price_guidance_1.PriceGuidance | None,
        Field(description='Optional pricing guidance for auction-based bidding'),
    ] = None
    parameters: Annotated[
        Parameters | None,
        Field(
            description='DOOH inventory allocation parameters. Sponsorship and takeover flat_rate options omit this field entirely — only include for digital out-of-home inventory.',
            title='DoohParameters',
        ),
    ] = None
    min_spend_per_package: Annotated[
        float | None,
        Field(
            description='Minimum spend requirement per package using this pricing option, in the specified currency',
            ge=0.0,
        ),
    ] = None
    price_breakdown: Annotated[
        price_breakdown_1.PriceBreakdown | None,
        Field(
            description='Breakdown of how fixed_price was derived from the list (rate card) price. Only meaningful when fixed_price is present.'
        ),
    ] = None
    eligible_adjustments: Annotated[
        list[adjustment_kind.PriceAdjustmentKind] | None,
        Field(
            description='Adjustment kinds applicable to this pricing option. Tells buyer agents which adjustments are available before negotiation. When absent, no adjustments are pre-declared — the buyer should check price_breakdown if present.'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var currency : str
var eligible_adjustments : list[adcp.types.generated_poc.enums.adjustment_kind.PriceAdjustmentKind] | None
var fixed_price : float | None
var floor_price : float | None
var min_spend_per_package : float | None
var model_config
var parameters : adcp.types.generated_poc.pricing_options.flat_rate_option.Parameters | None
var price_breakdown : adcp.types.generated_poc.pricing_options.price_breakdown.PriceBreakdown | None
var price_guidance : adcp.types.generated_poc.pricing_options.price_guidance.PriceGuidance | None
var pricing_model : Literal['flat_rate']
var pricing_option_id : str

Inherited members

class Format (**data: Any)
Expand source code
class Format(CanonicalBoundaryModel):
    """Canonical format declaration exposed as ``adcp.Format``."""

    format_option_id: str | None = Field(
        default=None,
        description="Stable option identifier within the product or publisher namespace.",
    )
    publisher_domain: str | None = Field(
        default=None,
        pattern=r"^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$",
    )
    display_name: str | None = None
    applies_to_channels: list[MediaChannel] | None = None
    seller_preference: SellerPreference | None = None
    canonical_formats_only: bool | None = None
    experimental: bool | None = None
    format_shape: str | None = None
    format_schema: PlatformExtensionReference | None = None
    format_kind: CanonicalFormatKind
    params: dict[str, Any]

    _legacy_format_refs: list[LegacyFormatId] = PrivateAttr(default_factory=list)

    _serialize_canonical = model_serializer(mode="wrap")(_serialize_canonical_model)

    @model_validator(mode="before")
    @classmethod
    def _reject_legacy_conflicts_and_credentials(cls, data: Any) -> Any:
        if not isinstance(data, dict):
            return data
        if data.get("canonical_formats_only") is True and data.get("v1_format_ref"):
            raise ValueError(
                "canonical_formats_only=True is mutually exclusive with legacy v1_format_ref"
            )
        for bag_name, bag in (
            ("params", data.get("params")),
            (
                "extras",
                {
                    key: value
                    for key, value in data.items()
                    if key not in cls.model_fields and key != "v1_format_ref"
                },
            ),
        ):
            found = _walk_for_credential_keys(bag, path=bag_name)
            if found is not None:
                raise ValueError(
                    f"{found!r} matches a credential-shaped key suffix and cannot "
                    "be stored in a canonical format declaration"
                )
        return data

    def __init__(self, **data: Any) -> None:
        refs = data.get("v1_format_ref")
        if "capability_id" in data and "format_option_id" not in data:
            data["format_option_id"] = data.pop("capability_id")
        super().__init__(**data)
        if self.__pydantic_extra__ is not None:
            self.__pydantic_extra__.pop("v1_format_ref", None)
        if refs:
            self._legacy_format_refs = [
                LegacyFormatId.model_validate(copy.deepcopy(ref)) for ref in refs
            ]

    @property
    def legacy_format_refs(self) -> tuple[LegacyFormatId, ...]:
        """Original tuples retained only for an explicit compatibility adapter."""

        return tuple(copy.deepcopy(ref) for ref in self._legacy_format_refs)

    def params_as(self, canonical_type: type[_CanonicalParamsT]) -> _CanonicalParamsT:
        """Validate the open parameter bag against a typed canonical model."""

        return canonical_type.model_validate(self.params)

    @model_validator(mode="after")
    def _validate_custom_shape(self) -> Format:
        if self.format_kind is CanonicalFormatKind.custom:
            if not self.format_shape:
                raise ValueError("custom formats require format_shape")
        elif self.format_shape is not None or self.format_schema is not None:
            raise ValueError("format_shape and format_schema are only valid for custom formats")
        return self

Canonical format declaration exposed as Format.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var applies_to_channels : list[adcp.types.generated_poc.enums.channels.MediaChannel] | None
var canonical_formats_only : bool | None
var display_name : str | None
var experimental : bool | None
var format_kind : adcp.types.generated_poc.core.canonical_format_kind.CanonicalFormatKind
var format_option_id : str | None
var format_schema : adcp.types.generated_poc.core.platform_extension_ref.PlatformExtensionReference | None
var format_shape : str | None
var model_config
var params : dict[str, typing.Any]
var publisher_domain : str | None
var seller_preference : adcp.types.generated_poc.core.product_format_declaration.SellerPreference | None

Instance variables

prop legacy_format_refs : tuple[LegacyFormatId, ...]
Expand source code
@property
def legacy_format_refs(self) -> tuple[LegacyFormatId, ...]:
    """Original tuples retained only for an explicit compatibility adapter."""

    return tuple(copy.deepcopy(ref) for ref in self._legacy_format_refs)

Original tuples retained only for an explicit compatibility adapter.

Methods

def model_post_init(self: BaseModel, context: Any, /) ‑> None
Expand source code
def init_private_attributes(self: BaseModel, context: Any, /) -> None:
    """This function is meant to behave like a BaseModel method to initialize private attributes.

    It takes context as an argument since that's what pydantic-core passes when calling it.

    Args:
        self: The BaseModel instance.
        context: The context.
    """
    if getattr(self, '__pydantic_private__', None) is None:
        pydantic_private = {}
        for name, private_attr in self.__private_attributes__.items():
            # Avoid needlessly creating a new dict for the validated data:
            if private_attr.default_factory_takes_validated_data:
                default = private_attr.get_default(
                    call_default_factory=True, validated_data={**self.__dict__, **pydantic_private}
                )
            else:
                default = private_attr.get_default(call_default_factory=True)
            if default is not PydanticUndefined:
                pydantic_private[name] = default
        object_setattr(self, '__pydantic_private__', pydantic_private)

This function is meant to behave like a BaseModel method to initialize private attributes.

It takes context as an argument since that's what pydantic-core passes when calling it.

Args
-----=
self
The BaseModel instance.
context
The context.
def params_as(self, canonical_type: type[_CanonicalParamsT]) ‑> ~_CanonicalParamsT
Expand source code
def params_as(self, canonical_type: type[_CanonicalParamsT]) -> _CanonicalParamsT:
    """Validate the open parameter bag against a typed canonical model."""

    return canonical_type.model_validate(self.params)

Validate the open parameter bag against a typed canonical model.

class LegacyFormat (**data: Any)
Expand source code
class Format(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    format_id: Annotated[
        format_id_1.FormatReferenceStructuredObject,
        Field(
            description="This format's own identifier — a structured object {agent_url, id}, not a string. See /schemas/core/format-id.json for the full shape."
        ),
    ]
    name: Annotated[str, Field(description='Human-readable format name')]
    description: Annotated[
        str | None,
        Field(
            description='Plain text explanation of what this format does and what assets it requires'
        ),
    ] = None
    example_url: Annotated[
        AnyUrl | None,
        Field(
            description='Optional URL to showcase page with examples and interactive demos of this format'
        ),
    ] = None
    accepts_parameters: Annotated[
        list[format_id_parameter.FormatIdParameter] | None,
        Field(
            description='List of parameters this format accepts in format_id. Template formats define which parameters (dimensions, duration, etc.) can be specified when instantiating the format. Empty or omitted means this is a concrete format with fixed parameters.'
        ),
    ] = None
    renders: Annotated[
        list[Renders | Renders1] | None,
        Field(
            description='Specification of rendered pieces for this format. Most formats produce a single render. Companion ad formats (video + banner), adaptive formats, and multi-placement formats produce multiple renders. Each render specifies its role and dimensions.',
            min_length=1,
        ),
    ] = None
    assets: Annotated[
        list[
            Assets
            | Assets11
            | Assets12
            | Assets13
            | Assets14
            | Assets15
            | Assets16
            | Assets17
            | Assets18
            | Assets19
            | Assets20
            | Assets21
            | Assets22
            | Assets23
            | Assets24
            | Assets25
        ]
        | None,
        Field(
            description="Array of all assets supported for this format. Each asset is identified by its asset_id, which must be used as the key in creative manifests. Use the 'required' boolean on each asset to indicate whether it's mandatory."
        ),
    ] = None
    delivery: Annotated[
        dict[str, Any] | None,
        Field(description='Delivery method specifications (e.g., hosted, VAST, third-party tags)'),
    ] = None
    supported_macros: Annotated[
        list[universal_macro.UniversalMacro | str] | None,
        Field(
            description='List of universal macros supported by this format (e.g., MEDIA_BUY_ID, CACHEBUSTER, DEVICE_ID). Used for validation and developer tooling. See docs/creative/universal-macros.mdx for full documentation.'
        ),
    ] = None
    input_format_ids: Annotated[
        list[format_id_1.FormatReferenceStructuredObject] | None,
        Field(
            deprecated=True,
            description='**DEPRECATED in 3.1. Removed at 4.0.** Use `list_transformers` instead — a transformer declares its own `input_format_ids`/`output_format_ids`, so build capability is a property of the transformer (the unit you select and that carries pricing), not a relationship hung on a format. Discover build capability via `list_transformers` (optionally filtered by `input_format_ids`/`output_format_ids`).\n\nMigration: sellers that expressed transform capability by hanging `input_format_ids` on a format SHOULD declare a transformer via `list_transformers` instead. Buyers SHOULD discover build capability via `list_transformers` rather than filtering formats.\n\n*Legacy behavior, retained for 3.1–3.x backward compatibility:* array of format IDs this format accepts as input creative manifests; when present, indicates this format can take existing creatives in these formats as input. SDKs reading 3.1 catalogs MUST continue to honor this field when present; 4.0+ SDKs MAY reject it. New code SHOULD NOT emit this field.',
        ),
    ] = None
    output_format_ids: Annotated[
        list[format_id_1.FormatReferenceStructuredObject] | None,
        Field(
            deprecated=True,
            description='**DEPRECATED in 3.1. Removed at 4.0.** Use `list_transformers` instead — a transformer declares its own `output_format_ids`, so what a builder can produce is a property of the transformer, not a relationship hung on a format. Discover via `list_transformers`.\n\nMigration: sellers that expressed multi-output build capability (e.g. a multi-publisher template) by hanging `output_format_ids` on a format SHOULD declare a transformer via `list_transformers` instead.\n\n*Legacy behavior, retained for 3.1–3.x backward compatibility:* array of format IDs this format can produce as output; when present, indicates this format can build creatives in these output formats. SDKs reading 3.1 catalogs MUST continue to honor this field when present; 4.0+ SDKs MAY reject it. New code SHOULD NOT emit this field.',
        ),
    ] = None
    format_card: Annotated[
        FormatCard | None,
        Field(
            description='Optional standard visual card (300x400px) for displaying this format in user interfaces. Can be rendered via preview_creative or pre-generated.'
        ),
    ] = None
    accessibility: Annotated[
        Accessibility | None,
        Field(
            description='Accessibility posture of this format. Declares the WCAG conformance level that creatives produced by this format will meet.'
        ),
    ] = None
    supported_disclosure_positions: Annotated[
        list[disclosure_position.DisclosurePosition] | None,
        Field(
            description='Disclosure positions this format can render. Buyers use this to determine whether a format can satisfy their compliance requirements before submitting a creative. When omitted, the format makes no disclosure rendering guarantees — creative agents SHOULD treat this as incompatible with briefs that require specific disclosure positions. Values correspond to positions on creative-brief.json required_disclosures.',
            min_length=1,
        ),
    ] = None
    disclosure_capabilities: Annotated[
        list[DisclosureCapability] | None,
        Field(
            description='Structured disclosure capabilities per position with persistence modes. Declares which persistence behaviors each disclosure position supports, enabling persistence-aware matching against provenance render guidance and brief requirements. When present, supersedes supported_disclosure_positions for persistence-aware queries. The flat supported_disclosure_positions field is retained for backward compatibility. Each position MUST appear at most once; validators and agents SHOULD reject duplicates.',
            min_length=1,
        ),
    ] = None
    format_card_detailed: Annotated[
        FormatCardDetailed | None,
        Field(
            description='Optional detailed card with carousel and full specifications. Provides rich format documentation similar to ad spec pages.'
        ),
    ] = None
    reported_metrics: Annotated[
        list[available_metric.AvailableMetric] | None,
        Field(
            description='Metrics this format can produce in delivery reporting. Buyers receive the intersection of format reported_metrics and product available_metrics. If omitted, the format defers entirely to product-level metric declarations.',
            min_length=1,
        ),
    ] = None
    pricing_options: Annotated[
        list[vendor_pricing_option.VendorPricingOption] | None,
        Field(
            deprecated=True,
            description='**DEPRECATED in 3.1. Removed at 4.0.** Use `transformer.pricing_options` (via `list_transformers`) instead — pricing belongs on the transformer (the unit selected and billed), exactly as it belongs on a media-buy product. Once formats only describe output shape, format-level pricing is vestigial.\n\nMigration: transformation/generation agents that charged via `format.pricing_options` SHOULD move the same `vendor-pricing-option` entries onto the corresponding transformer. The applied option is echoed per-leaf on the build_creative response and reconciled via report_usage, unchanged.\n\n*Legacy behavior, retained for 3.1–3.x backward compatibility:* pricing options for this format, used by transformation/generation agents that charge per format adapted, per image generated, or per unit of work; present when the request included include_pricing=true and account. SDKs reading 3.1 catalogs MUST continue to honor this field when present; 4.0+ SDKs MAY reject it. New code SHOULD NOT emit this field.',
            min_length=1,
        ),
    ] = None
    canonical: Annotated[
        canonical_projection_ref.CanonicalProjectionReference | None,
        Field(
            description='Optional v2 canonical-projection annotation. Always an object — bare-string shorthand (`canonical: "image"`) is not supported; the minimal form is `canonical: { "kind": "image" }`. Carries `kind` (which canonical the v1 format projects to) plus optional `asset_source` and `slots_override` for cases where the v1 format\'s shape doesn\'t follow the canonical\'s defaults (e.g., generative entries whose input is `generation_prompt: text` instead of `image_main: image`).\n\nWhen set, SDKs use this annotation as the authoritative v1 → v2 mapping for this format, bypassing the [v1 canonical mapping registry](/schemas/registries/v1-canonical-mapping.json) lookup. Combined with the slot-level `asset_group_id` declarations on each `assets[i]` entry, a v1 format declaration with `canonical` set is fully self-describing for v1↔v2 translation.\n\nResolution order for SDK projection from v1 wire shape to v2 (per RFC #3305 amendment #3767):\n1. If this `canonical` field is set, use it (seller-declared, highest priority). Apply `asset_source` and `slots_override` from the projection ref when present; otherwise inherit the canonical\'s defaults.\n2. Else, look up `format_id` in the canonical mapping registry\'s `format_id_glob` entries.\n3. Else, attempt structural match against the registry\'s `structural` entries (asset types, slot shape, vast_versions, etc.).\n4. Else, fail closed: SDK MUST NOT emit `format_options` for products carrying this format. Surface `FORMAT_PROJECTION_FAILED` on the response `errors[]` suggesting the seller add an explicit `canonical` annotation or file a registry entry.\n\nWhen `canonical.kind` is `custom`, the seller MUST also declare `canonical_format_shape` and `canonical_format_schema` (parallel to ProductFormatDeclaration\'s `format_shape` and `format_schema`) so buyer SDKs can fetch the seller\'s custom format schema.\n\nSee `canonical-projection-ref.json` for full projection semantics and examples (default-slot case, generative case, brief-driven case).'
        ),
    ] = None
    canonical_parameters: Annotated[
        product_format_declaration.ProductFormatDeclaration | None,
        Field(
            deprecated=True,
            description="**DEPRECATED in 3.1. Removed at 4.0.** Use `v1_format_ref` on the v2 `ProductFormatDeclaration` instead — the seller authors a v2 declaration (in `Product.format_options` or `creative.supported_formats`) and links it back to this v1 format via `v1_format_ref: { agent_url, id }`. The directional link from v2 → v1 is the same fact as `canonical_parameters` without the parallel-shape drift surface (v1 file and `canonical_parameters` were two declarations of the same thing; hand-authored, drifting silently).\n\nMigration: every seller currently authoring `canonical_parameters` SHOULD migrate to authoring a v2 declaration on the corresponding product (or capability) with `v1_format_ref` pointing back at this v1 format. v1 files become pure v1 again — no v2-shape mirroring.\n\n*Legacy behavior, retained for 3.1–3.x backward compatibility:* When `canonical` is set, this field carries the full ProductFormatDeclaration that the SDK projects this v1 format into. The `format_kind` MUST equal the `canonical` field value (validators enforce). When set, this is the authoritative source for SDK v1→v2 projection — the registry's structural-match parameter inference is bypassed. SDKs reading 3.1 catalogs MUST continue to honor `canonical_parameters` when present; 4.0+ SDKs MAY reject the field. New code SHOULD NOT emit this field.\n\n**Drift contract (still normative while supported).** Hand-authored `canonical_parameters` MUST satisfy the *narrows* relation against this v1 format's `requirements` and `assets[*]` shape (see canonical-formats.mdx 'Narrows — formal definition'). SDKs that read this v1 file SHOULD lint-time check the equivalence at build/load and emit `FORMAT_PROJECTION_FAILED` if the two disagree.",
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var accepts_parameters : list[adcp.types.generated_poc.enums.format_id_parameter.FormatIdParameter] | None
var accessibility : adcp.types.generated_poc.core.format.Accessibility | None
var assets : list[typing.Union[adcp.types.generated_poc.core.format.Assets, adcp.types.generated_poc.core.format.Assets11, adcp.types.generated_poc.core.format.Assets12, adcp.types.generated_poc.core.format.Assets13, adcp.types.generated_poc.core.format.Assets14, adcp.types.generated_poc.core.format.Assets15, adcp.types.generated_poc.core.format.Assets16, adcp.types.generated_poc.core.format.Assets17, adcp.types.generated_poc.core.format.Assets19, adcp.types.generated_poc.core.format.Assets20, adcp.types.generated_poc.core.format.Assets21, adcp.types.generated_poc.core.format.Assets22, adcp.types.generated_poc.core.format.Assets23, adcp.types.generated_poc.core.format.Assets24, adcp.types.generated_poc.core.format.Assets25, UnknownFormatAsset]] | None
var canonical : adcp.types.generated_poc.core.canonical_projection_ref.CanonicalProjectionReference | None
var delivery : dict[str, typing.Any] | None
var description : str | None
var disclosure_capabilities : list[adcp.types.generated_poc.core.format.DisclosureCapability] | None
var example_url : pydantic.networks.AnyUrl | None
var format_card : adcp.types.generated_poc.core.format.FormatCard | None
var format_card_detailed : adcp.types.generated_poc.core.format.FormatCardDetailed | None
var format_id : adcp.types.generated_poc.core.format_id.FormatReferenceStructuredObject
var model_config
var name : str
var renders : list[adcp.types.generated_poc.core.format.Renders | adcp.types.generated_poc.core.format.Renders1] | None
var reported_metrics : list[adcp.types.generated_poc.enums.available_metric.AvailableMetric] | None
var supported_disclosure_positions : list[adcp.types.generated_poc.enums.disclosure_position.DisclosurePosition] | None
var supported_macros : list[adcp.types.generated_poc.enums.universal_macro.UniversalMacro | str] | None

Instance variables

var canonical_parameters : adcp.types.generated_poc.core.product_format_declaration.ProductFormatDeclaration | None
Expand source code
def __get__(self, obj: BaseModel | None, obj_type: type[BaseModel] | None = None) -> Any:
    if obj is None:
        if self.wrapped_property is not None:
            return self.wrapped_property.__get__(None, obj_type)
        raise AttributeError(self.field_name)

    warnings.warn(self.msg, DeprecationWarning, stacklevel=2)

    if self.wrapped_property is not None:
        return self.wrapped_property.__get__(obj, obj_type)
    return obj.__dict__[self.field_name]

Read-only data descriptor used to emit a runtime deprecation warning before accessing a deprecated field.

Attributes
-----=
msg
The deprecation message to be emitted.
wrapped_property
The property instance if the deprecated field is a computed field, or None.
field_name
The name of the field being deprecated.
var input_format_ids : list[adcp.types.generated_poc.core.format_id.FormatReferenceStructuredObject] | None
Expand source code
def __get__(self, obj: BaseModel | None, obj_type: type[BaseModel] | None = None) -> Any:
    if obj is None:
        if self.wrapped_property is not None:
            return self.wrapped_property.__get__(None, obj_type)
        raise AttributeError(self.field_name)

    warnings.warn(self.msg, DeprecationWarning, stacklevel=2)

    if self.wrapped_property is not None:
        return self.wrapped_property.__get__(obj, obj_type)
    return obj.__dict__[self.field_name]

Read-only data descriptor used to emit a runtime deprecation warning before accessing a deprecated field.

Attributes
-----=
msg
The deprecation message to be emitted.
wrapped_property
The property instance if the deprecated field is a computed field, or None.
field_name
The name of the field being deprecated.
var output_format_ids : list[adcp.types.generated_poc.core.format_id.FormatReferenceStructuredObject] | None
Expand source code
def __get__(self, obj: BaseModel | None, obj_type: type[BaseModel] | None = None) -> Any:
    if obj is None:
        if self.wrapped_property is not None:
            return self.wrapped_property.__get__(None, obj_type)
        raise AttributeError(self.field_name)

    warnings.warn(self.msg, DeprecationWarning, stacklevel=2)

    if self.wrapped_property is not None:
        return self.wrapped_property.__get__(obj, obj_type)
    return obj.__dict__[self.field_name]

Read-only data descriptor used to emit a runtime deprecation warning before accessing a deprecated field.

Attributes
-----=
msg
The deprecation message to be emitted.
wrapped_property
The property instance if the deprecated field is a computed field, or None.
field_name
The name of the field being deprecated.
var pricing_options : list[adcp.types.generated_poc.core.vendor_pricing_option.VendorPricingOption] | None
Expand source code
def __get__(self, obj: BaseModel | None, obj_type: type[BaseModel] | None = None) -> Any:
    if obj is None:
        if self.wrapped_property is not None:
            return self.wrapped_property.__get__(None, obj_type)
        raise AttributeError(self.field_name)

    warnings.warn(self.msg, DeprecationWarning, stacklevel=2)

    if self.wrapped_property is not None:
        return self.wrapped_property.__get__(obj, obj_type)
    return obj.__dict__[self.field_name]

Read-only data descriptor used to emit a runtime deprecation warning before accessing a deprecated field.

Attributes
-----=
msg
The deprecation message to be emitted.
wrapped_property
The property instance if the deprecated field is a computed field, or None.
field_name
The name of the field being deprecated.
class ProductFormatDeclaration (**data: Any)
Expand source code
class Format(CanonicalBoundaryModel):
    """Canonical format declaration exposed as ``adcp.Format``."""

    format_option_id: str | None = Field(
        default=None,
        description="Stable option identifier within the product or publisher namespace.",
    )
    publisher_domain: str | None = Field(
        default=None,
        pattern=r"^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$",
    )
    display_name: str | None = None
    applies_to_channels: list[MediaChannel] | None = None
    seller_preference: SellerPreference | None = None
    canonical_formats_only: bool | None = None
    experimental: bool | None = None
    format_shape: str | None = None
    format_schema: PlatformExtensionReference | None = None
    format_kind: CanonicalFormatKind
    params: dict[str, Any]

    _legacy_format_refs: list[LegacyFormatId] = PrivateAttr(default_factory=list)

    _serialize_canonical = model_serializer(mode="wrap")(_serialize_canonical_model)

    @model_validator(mode="before")
    @classmethod
    def _reject_legacy_conflicts_and_credentials(cls, data: Any) -> Any:
        if not isinstance(data, dict):
            return data
        if data.get("canonical_formats_only") is True and data.get("v1_format_ref"):
            raise ValueError(
                "canonical_formats_only=True is mutually exclusive with legacy v1_format_ref"
            )
        for bag_name, bag in (
            ("params", data.get("params")),
            (
                "extras",
                {
                    key: value
                    for key, value in data.items()
                    if key not in cls.model_fields and key != "v1_format_ref"
                },
            ),
        ):
            found = _walk_for_credential_keys(bag, path=bag_name)
            if found is not None:
                raise ValueError(
                    f"{found!r} matches a credential-shaped key suffix and cannot "
                    "be stored in a canonical format declaration"
                )
        return data

    def __init__(self, **data: Any) -> None:
        refs = data.get("v1_format_ref")
        if "capability_id" in data and "format_option_id" not in data:
            data["format_option_id"] = data.pop("capability_id")
        super().__init__(**data)
        if self.__pydantic_extra__ is not None:
            self.__pydantic_extra__.pop("v1_format_ref", None)
        if refs:
            self._legacy_format_refs = [
                LegacyFormatId.model_validate(copy.deepcopy(ref)) for ref in refs
            ]

    @property
    def legacy_format_refs(self) -> tuple[LegacyFormatId, ...]:
        """Original tuples retained only for an explicit compatibility adapter."""

        return tuple(copy.deepcopy(ref) for ref in self._legacy_format_refs)

    def params_as(self, canonical_type: type[_CanonicalParamsT]) -> _CanonicalParamsT:
        """Validate the open parameter bag against a typed canonical model."""

        return canonical_type.model_validate(self.params)

    @model_validator(mode="after")
    def _validate_custom_shape(self) -> Format:
        if self.format_kind is CanonicalFormatKind.custom:
            if not self.format_shape:
                raise ValueError("custom formats require format_shape")
        elif self.format_shape is not None or self.format_schema is not None:
            raise ValueError("format_shape and format_schema are only valid for custom formats")
        return self

Canonical format declaration exposed as Format.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var applies_to_channels : list[adcp.types.generated_poc.enums.channels.MediaChannel] | None
var canonical_formats_only : bool | None
var display_name : str | None
var experimental : bool | None
var format_kind : adcp.types.generated_poc.core.canonical_format_kind.CanonicalFormatKind
var format_option_id : str | None
var format_schema : adcp.types.generated_poc.core.platform_extension_ref.PlatformExtensionReference | None
var format_shape : str | None
var model_config
var params : dict[str, typing.Any]
var publisher_domain : str | None
var seller_preference : adcp.types.generated_poc.core.product_format_declaration.SellerPreference | None

Instance variables

prop legacy_format_refs : tuple[LegacyFormatId, ...]
Expand source code
@property
def legacy_format_refs(self) -> tuple[LegacyFormatId, ...]:
    """Original tuples retained only for an explicit compatibility adapter."""

    return tuple(copy.deepcopy(ref) for ref in self._legacy_format_refs)

Original tuples retained only for an explicit compatibility adapter.

Methods

def model_post_init(self: BaseModel, context: Any, /) ‑> None
Expand source code
def init_private_attributes(self: BaseModel, context: Any, /) -> None:
    """This function is meant to behave like a BaseModel method to initialize private attributes.

    It takes context as an argument since that's what pydantic-core passes when calling it.

    Args:
        self: The BaseModel instance.
        context: The context.
    """
    if getattr(self, '__pydantic_private__', None) is None:
        pydantic_private = {}
        for name, private_attr in self.__private_attributes__.items():
            # Avoid needlessly creating a new dict for the validated data:
            if private_attr.default_factory_takes_validated_data:
                default = private_attr.get_default(
                    call_default_factory=True, validated_data={**self.__dict__, **pydantic_private}
                )
            else:
                default = private_attr.get_default(call_default_factory=True)
            if default is not PydanticUndefined:
                pydantic_private[name] = default
        object_setattr(self, '__pydantic_private__', pydantic_private)

This function is meant to behave like a BaseModel method to initialize private attributes.

It takes context as an argument since that's what pydantic-core passes when calling it.

Args
-----=
self
The BaseModel instance.
context
The context.
def params_as(self, canonical_type: type[_CanonicalParamsT]) ‑> ~_CanonicalParamsT
Expand source code
def params_as(self, canonical_type: type[_CanonicalParamsT]) -> _CanonicalParamsT:
    """Validate the open parameter bag against a typed canonical model."""

    return canonical_type.model_validate(self.params)

Validate the open parameter bag against a typed canonical model.

Inherited members

class FormatOptionReference (root: RootModelRootType = PydanticUndefined, **data)
Expand source code
class FormatOptionReference(RootModel[FormatOptionReference1 | FormatOptionReference2]):
    root: Annotated[
        FormatOptionReference1 | FormatOptionReference2,
        Field(
            description='Discriminated reference to a product format option. The global canonical shape is still named by `format_kind`; this reference selects one concrete product `format_options[]` entry. `scope: "publisher"` identifies a publisher-declared catalog option by `{ publisher_domain, format_option_id }`. `scope: "product"` identifies a product-local option by `format_option_id`; the enclosing package/product context supplies the namespace.',
            discriminator='scope',
            title='Format Option Reference',
        ),
    ]
    def __getattr__(self, name: str) -> Any:
        """Proxy attribute access to the wrapped type."""
        if name.startswith('_'):
            raise AttributeError(name)
        return getattr(self.root, name)

Usage Documentation

RootModel and Custom Root Types

A Pydantic BaseModel for the root object of the model.

Attributes
-----=
root
The root object of the model.
__pydantic_root_model__
Whether the model is a RootModel.
__pydantic_private__
Private fields in the model.
__pydantic_extra__
Extra fields in the model.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • pydantic.root_model.RootModel[Union[FormatOptionReference1, FormatOptionReference2]]
  • pydantic.root_model.RootModel
  • pydantic.main.BaseModel
  • typing.Generic

Class variables

var model_config
var root : adcp.types.generated_poc.core.format_option_ref.FormatOptionReference1 | adcp.types.generated_poc.core.format_option_ref.FormatOptionReference2
class GetAccountFinancialsRequest (**data: Any)
Expand source code
class GetAccountFinancialsRequest(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    account: Annotated[
        account_ref.AccountReference,
        Field(description='Account to query financials for. Must be an operator-billed account.'),
    ]
    period: Annotated[
        date_range.DateRange | None,
        Field(
            description='Date range for the spend summary. Defaults to the current billing cycle if omitted.'
        ),
    ] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var account : adcp.types.generated_poc.core.account_ref.AccountReference
var context : adcp.types.generated_poc.core.context.ContextObject | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config
var period : adcp.types.generated_poc.core.date_range.DateRange | None

Inherited members

class GetAccountFinancialsResponse1 (**data: Any)
Expand source code
class GetAccountFinancialsResponse1(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    account: account_ref_1.AccountReference
    currency: Annotated[str, StringConstraints(pattern='^[A-Z]{3}$')]
    period: date_range_1.DateRange
    timezone: str
    spend: Spend | None = None
    credit: Credit | None = None
    balance: Balance | None = None
    payment_status: Literal['current', 'past_due', 'suspended'] | None = None
    payment_terms: payment_terms_1.PaymentTerms | None = None
    invoices: list[Invoice] | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var account : adcp.types.generated_poc.core.account_ref.AccountReference
var balance : adcp.types.generated_poc.account.get_account_financials_response.Balance | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var credit : adcp.types.generated_poc.account.get_account_financials_response.Credit | None
var currency : str
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var invoices : list[adcp.types.generated_poc.account.get_account_financials_response.Invoice] | None
var model_config
var payment_status : Literal['current', 'past_due', 'suspended'] | None
var payment_terms : adcp.types.generated_poc.enums.payment_terms.PaymentTerms | None
var period : adcp.types.generated_poc.core.date_range.DateRange
var spend : adcp.types.generated_poc.account.get_account_financials_response.Spend | None
var timezone : str
class GetAccountFinancialsSuccessResponse (**data: Any)
Expand source code
class GetAccountFinancialsResponse1(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    account: account_ref_1.AccountReference
    currency: Annotated[str, StringConstraints(pattern='^[A-Z]{3}$')]
    period: date_range_1.DateRange
    timezone: str
    spend: Spend | None = None
    credit: Credit | None = None
    balance: Balance | None = None
    payment_status: Literal['current', 'past_due', 'suspended'] | None = None
    payment_terms: payment_terms_1.PaymentTerms | None = None
    invoices: list[Invoice] | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var account : adcp.types.generated_poc.core.account_ref.AccountReference
var balance : adcp.types.generated_poc.account.get_account_financials_response.Balance | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var credit : adcp.types.generated_poc.account.get_account_financials_response.Credit | None
var currency : str
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var invoices : list[adcp.types.generated_poc.account.get_account_financials_response.Invoice] | None
var model_config
var payment_status : Literal['current', 'past_due', 'suspended'] | None
var payment_terms : adcp.types.generated_poc.enums.payment_terms.PaymentTerms | None
var period : adcp.types.generated_poc.core.date_range.DateRange
var spend : adcp.types.generated_poc.account.get_account_financials_response.Spend | None
var timezone : str

Inherited members

class GetAccountFinancialsErrorResponse (**data: Any)
Expand source code
class GetAccountFinancialsResponse2(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    errors: Annotated[list[error_1.Error], Field(min_length=1)]
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var errors : list[adcp.types.generated_poc.core.error.Error]
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config

Inherited members

class GetBrandIdentityRequest (**data: Any)
Expand source code
class GetBrandIdentityRequest(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    brand_id: Annotated[str, Field(description='Brand identifier from brand.json brands array')]
    fields: Annotated[
        list[FieldModel] | None,
        Field(
            description='Optional identity sections to include in the response. When omitted, all sections the caller is authorized to see are returned. Core fields (brand_id, house, names) are always returned and do not need to be requested.',
            min_length=1,
        ),
    ] = None
    use_case: Annotated[
        str | None,
        Field(
            description="Intended use case, so the agent can tailor the response. A 'voice_synthesis' use case returns voice configs; a 'likeness' use case returns high-res photos and appearance guidelines."
        ),
    ] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var brand_id : str
var context : adcp.types.generated_poc.core.context.ContextObject | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var fields : list[adcp.types.generated_poc.brand.get_brand_identity_request.FieldModel] | None
var model_config
var use_case : str | None

Inherited members

class GetBrandIdentityResponse1 (**data: Any)
Expand source code
class GetBrandIdentityResponse1(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    brand_id: str
    house: House
    names: list[dict[str, str]]
    description: str | None = None
    industries: Annotated[list[str], Field(min_length=1)] | None = None
    keller_type: Literal['master', 'sub_brand', 'endorsed', 'independent'] | None = None
    logos: list[Logo] | None = None
    colors: Colors | None = None
    fonts: Fonts | None = None
    visual_guidelines: dict[str, Any] | None = None
    tone: Tone | None = None
    tagline: str | Annotated[list[dict[str, Annotated[str, StringConstraints(min_length=1)]]], Field(min_length=1)] | None = None
    voice_synthesis: VoiceSynthesis | None = None
    assets: list[Asset] | None = None
    rights: Rights | None = None
    available_fields: list[Literal['description', 'industries', 'keller_type', 'logos', 'colors', 'fonts', 'visual_guidelines', 'tone', 'tagline', 'voice_synthesis', 'assets', 'rights']] | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var assets : list[adcp.types.generated_poc.brand.get_brand_identity_response.Asset] | None
var available_fields : list[typing.Literal['description', 'industries', 'keller_type', 'logos', 'colors', 'fonts', 'visual_guidelines', 'tone', 'tagline', 'voice_synthesis', 'assets', 'rights']] | None
var brand_id : str
var colors : adcp.types.generated_poc.brand.get_brand_identity_response.Colors | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var description : str | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var fonts : adcp.types.generated_poc.brand.get_brand_identity_response.Fonts | None
var house : adcp.types.generated_poc.brand.get_brand_identity_response.House
var industries : list[str] | None
var keller_type : Literal['master', 'sub_brand', 'endorsed', 'independent'] | None
var logos : list[adcp.types.generated_poc.brand.get_brand_identity_response.Logo] | None
var model_config
var names : list[dict[str, str]]
var rights : adcp.types.generated_poc.brand.get_brand_identity_response.Rights | None
var tagline : str | list[dict[str, str]] | None
var tone : adcp.types.generated_poc.brand.get_brand_identity_response.Tone | None
var visual_guidelines : dict[str, typing.Any] | None
var voice_synthesis : adcp.types.generated_poc.brand.get_brand_identity_response.VoiceSynthesis | None
class GetBrandIdentitySuccessResponse (**data: Any)
Expand source code
class GetBrandIdentityResponse1(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    brand_id: str
    house: House
    names: list[dict[str, str]]
    description: str | None = None
    industries: Annotated[list[str], Field(min_length=1)] | None = None
    keller_type: Literal['master', 'sub_brand', 'endorsed', 'independent'] | None = None
    logos: list[Logo] | None = None
    colors: Colors | None = None
    fonts: Fonts | None = None
    visual_guidelines: dict[str, Any] | None = None
    tone: Tone | None = None
    tagline: str | Annotated[list[dict[str, Annotated[str, StringConstraints(min_length=1)]]], Field(min_length=1)] | None = None
    voice_synthesis: VoiceSynthesis | None = None
    assets: list[Asset] | None = None
    rights: Rights | None = None
    available_fields: list[Literal['description', 'industries', 'keller_type', 'logos', 'colors', 'fonts', 'visual_guidelines', 'tone', 'tagline', 'voice_synthesis', 'assets', 'rights']] | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var assets : list[adcp.types.generated_poc.brand.get_brand_identity_response.Asset] | None
var available_fields : list[typing.Literal['description', 'industries', 'keller_type', 'logos', 'colors', 'fonts', 'visual_guidelines', 'tone', 'tagline', 'voice_synthesis', 'assets', 'rights']] | None
var brand_id : str
var colors : adcp.types.generated_poc.brand.get_brand_identity_response.Colors | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var description : str | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var fonts : adcp.types.generated_poc.brand.get_brand_identity_response.Fonts | None
var house : adcp.types.generated_poc.brand.get_brand_identity_response.House
var industries : list[str] | None
var keller_type : Literal['master', 'sub_brand', 'endorsed', 'independent'] | None
var logos : list[adcp.types.generated_poc.brand.get_brand_identity_response.Logo] | None
var model_config
var names : list[dict[str, str]]
var rights : adcp.types.generated_poc.brand.get_brand_identity_response.Rights | None
var tagline : str | list[dict[str, str]] | None
var tone : adcp.types.generated_poc.brand.get_brand_identity_response.Tone | None
var visual_guidelines : dict[str, typing.Any] | None
var voice_synthesis : adcp.types.generated_poc.brand.get_brand_identity_response.VoiceSynthesis | None

Inherited members

class GetBrandIdentityErrorResponse (**data: Any)
Expand source code
class GetBrandIdentityResponse2(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    errors: Annotated[list[error_1.Error], Field(min_length=1)]
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var errors : list[adcp.types.generated_poc.core.error.Error]
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config

Inherited members

class GetContentStandardsSuccessResponse (**data: Any)
Expand source code
class GetContentStandardsResponse1(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config
class GetContentStandardsResponse1 (**data: Any)
Expand source code
class GetContentStandardsResponse1(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config

Inherited members

class GetContentStandardsErrorResponse (**data: Any)
Expand source code
class GetContentStandardsResponse2(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    errors: list[error_1.Error]
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var errors : list[adcp.types.generated_poc.core.error.Error]
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config

Inherited members

class GetCreativeDeliveryRequest (**data: Any)
Expand source code
class GetCreativeDeliveryRequest(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    account: Annotated[
        account_ref.AccountReference | None,
        Field(
            description='Account for routing and scoping. Limits results to creatives within this account.'
        ),
    ] = None
    media_buy_ids: Annotated[
        list[str] | None,
        Field(
            description='Filter to specific media buys by publisher ID. If omitted, returns creative delivery across all matching media buys.',
            min_length=1,
        ),
    ] = None
    creative_ids: Annotated[
        list[str] | None,
        Field(
            description='Filter to specific creatives by ID. If omitted, returns delivery for all creatives matching the other filters.',
            min_length=1,
        ),
    ] = None
    start_date: Annotated[
        str | None,
        Field(
            description="Start date for delivery period (YYYY-MM-DD). Interpreted in the platform's reporting timezone.",
            pattern='^\\d{4}-\\d{2}-\\d{2}$',
        ),
    ] = None
    end_date: Annotated[
        str | None,
        Field(
            description="End date for delivery period (YYYY-MM-DD). Interpreted in the platform's reporting timezone.",
            pattern='^\\d{4}-\\d{2}-\\d{2}$',
        ),
    ] = None
    max_variants: Annotated[
        int | None,
        Field(
            description='Maximum number of variants to return per creative. When omitted, the agent returns all variants. Use this to limit response size for generative creatives that may produce large numbers of variants.',
            ge=1,
        ),
    ] = None
    pagination: Annotated[
        pagination_request.PaginationRequest | None,
        Field(
            description='Pagination parameters for the creatives array in the response. Uses cursor-based pagination consistent with other list operations.'
        ),
    ] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var account : adcp.types.generated_poc.core.account_ref.AccountReference | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var creative_ids : list[str] | None
var end_date : str | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var max_variants : int | None
var media_buy_ids : list[str] | None
var model_config
var pagination : adcp.types.generated_poc.core.pagination_request.PaginationRequest | None
var start_date : str | None
class GetCreativeDeliveryByBuyerRefRequest (**data: Any)
Expand source code
class GetCreativeDeliveryRequest(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    account: Annotated[
        account_ref.AccountReference | None,
        Field(
            description='Account for routing and scoping. Limits results to creatives within this account.'
        ),
    ] = None
    media_buy_ids: Annotated[
        list[str] | None,
        Field(
            description='Filter to specific media buys by publisher ID. If omitted, returns creative delivery across all matching media buys.',
            min_length=1,
        ),
    ] = None
    creative_ids: Annotated[
        list[str] | None,
        Field(
            description='Filter to specific creatives by ID. If omitted, returns delivery for all creatives matching the other filters.',
            min_length=1,
        ),
    ] = None
    start_date: Annotated[
        str | None,
        Field(
            description="Start date for delivery period (YYYY-MM-DD). Interpreted in the platform's reporting timezone.",
            pattern='^\\d{4}-\\d{2}-\\d{2}$',
        ),
    ] = None
    end_date: Annotated[
        str | None,
        Field(
            description="End date for delivery period (YYYY-MM-DD). Interpreted in the platform's reporting timezone.",
            pattern='^\\d{4}-\\d{2}-\\d{2}$',
        ),
    ] = None
    max_variants: Annotated[
        int | None,
        Field(
            description='Maximum number of variants to return per creative. When omitted, the agent returns all variants. Use this to limit response size for generative creatives that may produce large numbers of variants.',
            ge=1,
        ),
    ] = None
    pagination: Annotated[
        pagination_request.PaginationRequest | None,
        Field(
            description='Pagination parameters for the creatives array in the response. Uses cursor-based pagination consistent with other list operations.'
        ),
    ] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var account : adcp.types.generated_poc.core.account_ref.AccountReference | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var creative_ids : list[str] | None
var end_date : str | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var max_variants : int | None
var media_buy_ids : list[str] | None
var model_config
var pagination : adcp.types.generated_poc.core.pagination_request.PaginationRequest | None
var start_date : str | None
class GetCreativeDeliveryByCreativeRequest (**data: Any)
Expand source code
class GetCreativeDeliveryRequest(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    account: Annotated[
        account_ref.AccountReference | None,
        Field(
            description='Account for routing and scoping. Limits results to creatives within this account.'
        ),
    ] = None
    media_buy_ids: Annotated[
        list[str] | None,
        Field(
            description='Filter to specific media buys by publisher ID. If omitted, returns creative delivery across all matching media buys.',
            min_length=1,
        ),
    ] = None
    creative_ids: Annotated[
        list[str] | None,
        Field(
            description='Filter to specific creatives by ID. If omitted, returns delivery for all creatives matching the other filters.',
            min_length=1,
        ),
    ] = None
    start_date: Annotated[
        str | None,
        Field(
            description="Start date for delivery period (YYYY-MM-DD). Interpreted in the platform's reporting timezone.",
            pattern='^\\d{4}-\\d{2}-\\d{2}$',
        ),
    ] = None
    end_date: Annotated[
        str | None,
        Field(
            description="End date for delivery period (YYYY-MM-DD). Interpreted in the platform's reporting timezone.",
            pattern='^\\d{4}-\\d{2}-\\d{2}$',
        ),
    ] = None
    max_variants: Annotated[
        int | None,
        Field(
            description='Maximum number of variants to return per creative. When omitted, the agent returns all variants. Use this to limit response size for generative creatives that may produce large numbers of variants.',
            ge=1,
        ),
    ] = None
    pagination: Annotated[
        pagination_request.PaginationRequest | None,
        Field(
            description='Pagination parameters for the creatives array in the response. Uses cursor-based pagination consistent with other list operations.'
        ),
    ] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var account : adcp.types.generated_poc.core.account_ref.AccountReference | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var creative_ids : list[str] | None
var end_date : str | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var max_variants : int | None
var media_buy_ids : list[str] | None
var model_config
var pagination : adcp.types.generated_poc.core.pagination_request.PaginationRequest | None
var start_date : str | None
class GetCreativeDeliveryByMediaBuyRequest (**data: Any)
Expand source code
class GetCreativeDeliveryRequest(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    account: Annotated[
        account_ref.AccountReference | None,
        Field(
            description='Account for routing and scoping. Limits results to creatives within this account.'
        ),
    ] = None
    media_buy_ids: Annotated[
        list[str] | None,
        Field(
            description='Filter to specific media buys by publisher ID. If omitted, returns creative delivery across all matching media buys.',
            min_length=1,
        ),
    ] = None
    creative_ids: Annotated[
        list[str] | None,
        Field(
            description='Filter to specific creatives by ID. If omitted, returns delivery for all creatives matching the other filters.',
            min_length=1,
        ),
    ] = None
    start_date: Annotated[
        str | None,
        Field(
            description="Start date for delivery period (YYYY-MM-DD). Interpreted in the platform's reporting timezone.",
            pattern='^\\d{4}-\\d{2}-\\d{2}$',
        ),
    ] = None
    end_date: Annotated[
        str | None,
        Field(
            description="End date for delivery period (YYYY-MM-DD). Interpreted in the platform's reporting timezone.",
            pattern='^\\d{4}-\\d{2}-\\d{2}$',
        ),
    ] = None
    max_variants: Annotated[
        int | None,
        Field(
            description='Maximum number of variants to return per creative. When omitted, the agent returns all variants. Use this to limit response size for generative creatives that may produce large numbers of variants.',
            ge=1,
        ),
    ] = None
    pagination: Annotated[
        pagination_request.PaginationRequest | None,
        Field(
            description='Pagination parameters for the creatives array in the response. Uses cursor-based pagination consistent with other list operations.'
        ),
    ] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var account : adcp.types.generated_poc.core.account_ref.AccountReference | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var creative_ids : list[str] | None
var end_date : str | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var max_variants : int | None
var media_buy_ids : list[str] | None
var model_config
var pagination : adcp.types.generated_poc.core.pagination_request.PaginationRequest | None
var start_date : str | None

Inherited members

class GetCreativeDeliveryResponse (**data: Any)

Base class enforcing the primary canonical runtime boundary.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var account_id : str | None
var adcp_error : adcp.types.generated_poc.core.error.Error | None
var adcp_major_version : int | None
var adcp_version : str | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var context_id : str | None
var creatives : Sequence[DeliveryCreative]
var currency : str
var errors : list[adcp.types.generated_poc.core.error.Error] | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var governance_context : str | None
var media_buy_id : str | None
var message : str | None
var model_config
var pagination : adcp.types.generated_poc.creative.get_creative_delivery_response.Pagination | None
var payload : dict[str, typing.Any] | None
var push_notification_config : adcp.types.generated_poc.core.push_notification_config.PushNotificationConfig | None
var replayed : bool | None
var reporting_period : adcp.types.generated_poc.creative.get_creative_delivery_response.ReportingPeriod
var status : adcp.types.generated_poc.enums.task_status.TaskStatus
var task_id : str | None
var timestamp : pydantic.types.AwareDatetime | None
class LegacyGetCreativeDeliveryResponse (**data: Any)
Expand source code
class GetCreativeDeliveryResponse(AdcpVersionEnvelope, ProtocolEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    account_id: Annotated[
        str | None,
        Field(
            description='Account identifier. Present when the response spans or is scoped to a specific account.'
        ),
    ] = None
    media_buy_id: Annotated[
        str | None,
        Field(
            description="Publisher's media buy identifier. Present when the request was scoped to a single media buy."
        ),
    ] = None
    currency: Annotated[
        str,
        Field(
            description="ISO 4217 currency code for monetary values in this response (e.g., 'USD', 'EUR')",
            pattern='^[A-Z]{3}$',
        ),
    ]
    reporting_period: Annotated[ReportingPeriod, Field(description='Date range for the report.')]
    creatives: Annotated[
        Sequence[Creative], Field(description='Creative delivery data with variant breakdowns')
    ]
    pagination: Annotated[
        Pagination | None,
        Field(
            description='Pagination information. Present when the request included pagination parameters. **Note:** `get_creative_delivery` uses page-based pagination (`limit`/`offset`) for historical reasons, distinct from the cursor-based [`PaginationResponse`](/schemas/v3/core/pagination-response.json) used by `list_*` tools. Field naming aligned with `PaginationResponse.total_count` in 3.1; the legacy `total` field is retained as a deprecated alias until 4.0. Sellers MUST populate both fields identically; buyers SHOULD prefer `total_count` (the canonical name) and ignore `total` if both are present.'
        ),
    ] = None
    errors: Annotated[
        list[error.Error] | None, Field(description='Task-specific errors and warnings')
    ] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • adcp.types.generated_poc.core.protocol_envelope.ProtocolEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var account_id : str | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var creatives : Sequence[adcp.types.generated_poc.creative.get_creative_delivery_response.Creative]
var currency : str
var errors : list[adcp.types.generated_poc.core.error.Error] | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var media_buy_id : str | None
var model_config
var pagination : adcp.types.generated_poc.creative.get_creative_delivery_response.Pagination | None
var reporting_period : adcp.types.generated_poc.creative.get_creative_delivery_response.ReportingPeriod

Inherited members

class GetCreativeFeaturesRequest (**data: Any)
Expand source code
class GetCreativeFeaturesRequest(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    creative_manifest: Annotated[
        creative_manifest_1.CreativeManifest,
        Field(
            description='The canonical creative manifest to evaluate. In 3.2 it contains `format_kind`, optional `format_option_ref`, and typed assets. The deprecated named `format_id` branch remains available only for older 3.x peers.'
        ),
    ]
    feature_ids: Annotated[
        list[str] | None,
        Field(
            description='Optional filter to specific features. If omitted, returns all available features.',
            min_length=1,
        ),
    ] = None
    account: Annotated[
        account_ref.AccountReference | None,
        Field(
            description='Account for billing this evaluation. Required when the governance agent charges per evaluation.'
        ),
    ] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var account : adcp.types.generated_poc.core.account_ref.AccountReference | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var creative_manifest : adcp.types.generated_poc.core.creative_manifest.CreativeManifest
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var feature_ids : list[str] | None
var model_config

Inherited members

class GetCreativeFeaturesResponse1 (**data: Any)
Expand source code
class GetCreativeFeaturesResponse1(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    results: list[creative_feature_result_1.CreativeFeatureResult]
    detail_url: AnyUrl | None = None
    audit_observations: list[audit_observation_1.CreativeAuditObservation] | None = None
    pricing_option_id: str | None = None
    vendor_cost: Annotated[float, Field(ge=0)] | None = None
    currency: Annotated[str, StringConstraints(pattern='^[A-Z]{3}$')] | None = None
    consumption: creative_consumption_1.CreativeConsumption | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var audit_observations : list[adcp.types.generated_poc.creative.audit_observation.CreativeAuditObservation] | None
var consumption : adcp.types.generated_poc.core.creative_consumption.CreativeConsumption | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var currency : str | None
var detail_url : pydantic.networks.AnyUrl | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config
var pricing_option_id : str | None
var results : list[adcp.types.generated_poc.creative.creative_feature_result.CreativeFeatureResult]
var vendor_cost : float | None
class GetCreativeFeaturesSuccessResponse (**data: Any)
Expand source code
class GetCreativeFeaturesResponse1(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    results: list[creative_feature_result_1.CreativeFeatureResult]
    detail_url: AnyUrl | None = None
    audit_observations: list[audit_observation_1.CreativeAuditObservation] | None = None
    pricing_option_id: str | None = None
    vendor_cost: Annotated[float, Field(ge=0)] | None = None
    currency: Annotated[str, StringConstraints(pattern='^[A-Z]{3}$')] | None = None
    consumption: creative_consumption_1.CreativeConsumption | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var audit_observations : list[adcp.types.generated_poc.creative.audit_observation.CreativeAuditObservation] | None
var consumption : adcp.types.generated_poc.core.creative_consumption.CreativeConsumption | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var currency : str | None
var detail_url : pydantic.networks.AnyUrl | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config
var pricing_option_id : str | None
var results : list[adcp.types.generated_poc.creative.creative_feature_result.CreativeFeatureResult]
var vendor_cost : float | None

Inherited members

class GetCreativeFeaturesErrorResponse (**data: Any)
Expand source code
class GetCreativeFeaturesResponse2(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    errors: list[error_1.Error]
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var errors : list[adcp.types.generated_poc.core.error.Error]
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config

Inherited members

class GetMediaBuyArtifactsSuccessResponse (**data: Any)
Expand source code
class GetMediaBuyArtifactsResponse1(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    media_buy_id: str
    artifacts: list[Artifact]
    collection_info: CollectionInfo | None = None
    pagination: pagination_response_1.PaginationResponse | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var artifacts : list[adcp.types.generated_poc.content_standards.get_media_buy_artifacts_response.Artifact]
var collection_info : adcp.types.generated_poc.content_standards.get_media_buy_artifacts_response.CollectionInfo | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var media_buy_id : str
var model_config
var pagination : adcp.types.generated_poc.core.pagination_response.PaginationResponse | None
class GetMediaBuyArtifactsResponse1 (**data: Any)
Expand source code
class GetMediaBuyArtifactsResponse1(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    media_buy_id: str
    artifacts: list[Artifact]
    collection_info: CollectionInfo | None = None
    pagination: pagination_response_1.PaginationResponse | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var artifacts : list[adcp.types.generated_poc.content_standards.get_media_buy_artifacts_response.Artifact]
var collection_info : adcp.types.generated_poc.content_standards.get_media_buy_artifacts_response.CollectionInfo | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var media_buy_id : str
var model_config
var pagination : adcp.types.generated_poc.core.pagination_response.PaginationResponse | None

Inherited members

class GetMediaBuyArtifactsErrorResponse (**data: Any)
Expand source code
class GetMediaBuyArtifactsResponse2(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    errors: list[error_1.Error]
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var errors : list[adcp.types.generated_poc.core.error.Error]
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config

Inherited members

class GetMediaBuyDeliveryRequest (**data: Any)
Expand source code
class GetMediaBuyDeliveryRequest(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    account: Annotated[
        account_ref.AccountReference | None,
        Field(
            description='Filter delivery data to a specific account. When omitted, returns data across all accessible accounts.'
        ),
    ] = None
    media_buy_ids: Annotated[
        list[str] | None,
        Field(description='Array of media buy IDs to get delivery data for', min_length=1),
    ] = None
    status_filter: Annotated[
        media_buy_status.MediaBuyStatus | StatusFilter | None,
        Field(description='Filter by status. Can be a single status or array of statuses'),
    ] = None
    start_date: Annotated[
        str | None,
        Field(
            description="Inclusive start date for the reporting period (YYYY-MM-DD). When omitted along with end_date, returns campaign lifetime data. Only accepted when the product's reporting_capabilities.date_range_support is 'date_range'.",
            pattern='^\\d{4}-\\d{2}-\\d{2}$',
        ),
    ] = None
    end_date: Annotated[
        str | None,
        Field(
            description="Exclusive end date for the reporting period (YYYY-MM-DD). Must be later than start_date. When omitted along with start_date, returns campaign lifetime data. Only accepted when the product's reporting_capabilities.date_range_support is 'date_range'.",
            pattern='^\\d{4}-\\d{2}-\\d{2}$',
        ),
    ] = None
    include_package_daily_breakdown: Annotated[
        bool | None,
        Field(
            description='When true, include daily_breakdown arrays within each package in by_package. Useful for per-package pacing analysis and line-item monitoring. Omit or set false to reduce response size — package daily data can be large for multi-package buys over long flights.'
        ),
    ] = False
    time_granularity: Annotated[
        reporting_frequency.ReportingFrequency | None,
        Field(
            description="Per-window slice granularity for the pull, using the same vocabulary as reporting_webhook.reporting_frequency. When set, the seller returns per-window delivery slices over the date range — useful for reconstructing data a buyer's webhook receiver missed, since the slice payload is shape-aligned with what reporting_webhook would have delivered for the same window. Capability-scoped: the value MUST be one of the seller's declared reporting_capabilities.windowed_pull_granularities; otherwise the seller MUST return UNSUPPORTED_GRANULARITY. When omitted, behavior is unchanged (cumulative aggregates plus optional daily breakdowns per existing fields)."
        ),
    ] = None
    include_window_breakdown: Annotated[
        bool | None,
        Field(
            description="When true, the response includes media_buy_deliveries[].windows[] — an array of per-window delivery slices over the date range at the requested time_granularity. Ignored when time_granularity is omitted. Each window's payload mirrors what reporting_webhook would have delivered for the same window, enabling lossless GET-path recovery for buyers who missed webhook fires. Omit or set false to reduce response size when only cumulative aggregates are needed."
        ),
    ] = False
    attribution_window: Annotated[
        AttributionWindow | None,
        Field(
            description='Attribution window to apply for conversion metrics. When provided, the seller returns conversion data using the requested lookback windows instead of their platform default. The seller echoes the applied window in the response. Sellers that do not support configurable windows ignore this field and return their default. Check get_adcp_capabilities conversion_tracking.attribution_windows for available options.'
        ),
    ] = None
    reporting_dimensions: Annotated[
        ReportingDimensions | None,
        Field(
            description='Request dimensional breakdowns in delivery reporting. Each key enables a specific breakdown dimension within by_package — include as an empty object (e.g., "device_type": {}) to activate with defaults. Omit entirely for no breakdowns (backward compatible). Unsupported dimensions are silently omitted from the response. Note: keyword, catalog_item, and creative breakdowns are returned automatically when the seller supports them and are not controlled by this object.'
        ),
    ] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var account : adcp.types.generated_poc.core.account_ref.AccountReference | None
var attribution_window : adcp.types.generated_poc.media_buy.get_media_buy_delivery_request.AttributionWindow | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var end_date : str | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var include_package_daily_breakdown : bool | None
var include_window_breakdown : bool | None
var media_buy_ids : list[str] | None
var model_config
var reporting_dimensions : adcp.types.generated_poc.media_buy.get_media_buy_delivery_request.ReportingDimensions | None
var start_date : str | None
var status_filter : adcp.types.generated_poc.enums.media_buy_status.MediaBuyStatus | adcp.types.generated_poc.media_buy.get_media_buy_delivery_request.StatusFilter | None
var time_granularity : adcp.types.generated_poc.enums.reporting_frequency.ReportingFrequency | None

Inherited members

class GetMediaBuyDeliveryResponse (**data: Any)

Base class enforcing the primary canonical runtime boundary.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var adcp_error : adcp.types.generated_poc.core.error.Error | None
var adcp_major_version : int | None
var adcp_version : str | None
var aggregated_totals : adcp.types.generated_poc.media_buy.get_media_buy_delivery_response.AggregatedTotals | None
var attribution_window : adcp.types.generated_poc.core.attribution_window.AttributionWindow | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var context_id : str | None
var currency : str
var errors : list[adcp.types.generated_poc.core.error.Error] | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var governance_context : str | None
var media_buy_deliveries : Sequence[adcp.types.generated_poc.media_buy.get_media_buy_delivery_response.MediaBuyDelivery]
var message : str | None
var model_config
var next_expected_at : pydantic.types.AwareDatetime | None
var notification_type : adcp.types.generated_poc.media_buy.get_media_buy_delivery_response.NotificationType | None
var partial_data : bool | None
var payload : dict[str, typing.Any] | None
var push_notification_config : adcp.types.generated_poc.core.push_notification_config.PushNotificationConfig | None
var replayed : bool | None
var reporting_period : adcp.types.generated_poc.media_buy.get_media_buy_delivery_response.ReportingPeriod
var sandbox : bool | None
var sequence_number : int | None
var status : adcp.types.generated_poc.enums.task_status.TaskStatus
var task_id : str | None
var timestamp : pydantic.types.AwareDatetime | None
var unavailable_count : int | None
class LegacyGetMediaBuyDeliveryResponse (**data: Any)
Expand source code
class GetMediaBuyDeliveryResponse(AdcpVersionEnvelope, ProtocolEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    notification_type: Annotated[
        NotificationType | None,
        Field(
            description='Type of webhook notification (only present in webhook deliveries): scheduled = regular periodic update, final = campaign completed, delayed = data not yet available, adjusted = resending period with corrected data (same window), window_update = resending period with a wider measurement window (e.g., C3 superseding live, C7 superseding C3)'
        ),
    ] = None
    partial_data: Annotated[
        bool | None,
        Field(
            description='Indicates if any media buys in this webhook have missing/delayed data (only present in webhook deliveries)'
        ),
    ] = None
    unavailable_count: Annotated[
        int | None,
        Field(
            description='Number of media buys with reporting_delayed or failed status (only present in webhook deliveries when partial_data is true)',
            ge=0,
        ),
    ] = None
    sequence_number: Annotated[
        int | None,
        Field(
            description='Sequential notification number (only present in webhook deliveries, starts at 1)',
            ge=1,
        ),
    ] = None
    next_expected_at: Annotated[
        AwareDatetime | None,
        Field(
            description="ISO 8601 timestamp for next expected notification (only present in webhook deliveries when notification_type is not 'final')"
        ),
    ] = None
    reporting_period: Annotated[
        ReportingPeriod,
        Field(
            description='Half-open date range for the report: start is inclusive and end is exclusive. All periods use UTC timezone.'
        ),
    ]
    currency: Annotated[str, Field(description='ISO 4217 currency code', pattern='^[A-Z]{3}$')]
    attribution_window: Annotated[
        attribution_window_1.AttributionWindow | None,
        Field(
            description='Attribution methodology and lookback windows used for conversion metrics in this response. All media buys from a single seller share the same attribution methodology. Enables cross-platform comparison (e.g., Amazon 14-day click vs. Criteo 30-day click).'
        ),
    ] = None
    aggregated_totals: Annotated[
        AggregatedTotals | None,
        Field(
            description='Combined metrics across all returned media buys. Only included in API responses (get_media_buy_delivery), not in webhook notifications.'
        ),
    ] = None
    media_buy_deliveries: Annotated[
        Sequence[MediaBuyDelivery],
        Field(
            description='Array of delivery data for media buys. When used in webhook notifications, may contain multiple media buys aggregated by publisher. When used in get_media_buy_delivery API responses, typically contains requested media buys.'
        ),
    ]
    errors: Annotated[
        list[error.Error] | None,
        Field(
            description='Task-specific errors and warnings (e.g., missing delivery data, reporting platform issues)'
        ),
    ] = None
    sandbox: Annotated[
        bool | None,
        Field(description='When true, this response contains simulated data from sandbox mode.'),
    ] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • adcp.types.generated_poc.core.protocol_envelope.ProtocolEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var aggregated_totals : adcp.types.generated_poc.media_buy.get_media_buy_delivery_response.AggregatedTotals | None
var attribution_window : adcp.types.generated_poc.core.attribution_window.AttributionWindow | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var currency : str
var errors : list[adcp.types.generated_poc.core.error.Error] | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var media_buy_deliveries : Sequence[adcp.types.generated_poc.media_buy.get_media_buy_delivery_response.MediaBuyDelivery]
var model_config
var next_expected_at : pydantic.types.AwareDatetime | None
var notification_type : adcp.types.generated_poc.media_buy.get_media_buy_delivery_response.NotificationType | None
var partial_data : bool | None
var reporting_period : adcp.types.generated_poc.media_buy.get_media_buy_delivery_response.ReportingPeriod
var sandbox : bool | None
var sequence_number : int | None
var status : adcp.types.generated_poc.enums.task_status.TaskStatus | None
var unavailable_count : int | None

Instance variables

var adcp_major_version : int | None
Expand source code
def __get__(self, obj: BaseModel | None, obj_type: type[BaseModel] | None = None) -> Any:
    if obj is None:
        if self.wrapped_property is not None:
            return self.wrapped_property.__get__(None, obj_type)
        raise AttributeError(self.field_name)

    warnings.warn(self.msg, DeprecationWarning, stacklevel=2)

    if self.wrapped_property is not None:
        return self.wrapped_property.__get__(obj, obj_type)
    return obj.__dict__[self.field_name]

Read-only data descriptor used to emit a runtime deprecation warning before accessing a deprecated field.

Attributes
-----=
msg
The deprecation message to be emitted.
wrapped_property
The property instance if the deprecated field is a computed field, or None.
field_name
The name of the field being deprecated.

Inherited members

class GetMediaBuysRequest (**data: Any)
Expand source code
class GetMediaBuysRequest(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    account: Annotated[
        account_ref.AccountReference | None,
        Field(
            description='Account to retrieve media buys for. When omitted, returns data across all accessible accounts.'
        ),
    ] = None
    media_buy_ids: Annotated[
        list[str] | None,
        Field(
            description='Array of media buy IDs to retrieve. When omitted, returns a paginated set of accessible media buys matching status_filter.',
            min_length=1,
        ),
    ] = None
    status_filter: Annotated[
        media_buy_status.MediaBuyStatus | StatusFilter | None,
        Field(
            description='Filter by status. Can be a single status or array of statuses. Defaults to ["active"] when media_buy_ids is omitted. When media_buy_ids is provided, no implicit status filter is applied.'
        ),
    ] = None
    indicator_types: Annotated[
        list[indicator_type.IndicatorType] | None,
        Field(
            description='Return media buys with at least one matching current indicator on the media buy, a package, or a package–creative assignment. Values within this field use OR logic; this field composes with status_filter using AND logic. Buyers MUST NOT send this filter unless the seller advertises every requested type in get_adcp_capabilities.media_buy.supported_indicator_types. A seller MAY reject a request that violates this precondition with UNSUPPORTED_FEATURE rather than silently returning an unfiltered superset.',
            min_length=1,
        ),
    ] = None
    include_snapshot: Annotated[
        bool | None,
        Field(
            description='When true, include a near-real-time delivery snapshot for each package. Snapshots reflect the latest available entity-level stats from the platform (e.g., updated every ~15 minutes on GAM, ~1 hour on batch-only platforms). The staleness_seconds field on each snapshot indicates data freshness. If a snapshot cannot be returned, package.snapshot_unavailable_reason explains why. Defaults to false.'
        ),
    ] = False
    include_history: Annotated[
        int | None,
        Field(
            description='When present, include the last N revision history entries for each media buy (returns min(N, available entries)). Each entry contains revision number, timestamp, actor, and a summary of what changed. Omit or set to 0 to exclude history (default). Recommended: 5-10 for monitoring, 50+ for audit.',
            ge=0,
            le=1000,
        ),
    ] = 0
    include_webhook_activity: Annotated[
        bool | None,
        Field(
            description="When true, each returned media buy includes a `webhook_activity` array describing recent delivery-report webhook fires for the calling principal. Used by buyer agents to verify whether a publisher actually fired against the buyer's registered endpoint and what the endpoint returned — closes the operator-ticket loop for webhook debugging. Scoped to the calling principal: a buyer sees only fires targeting its own endpoint, even when multiple principals share visibility into the same media buy. Defaults to false. See `webhook_activity_limit` for the per-buy cap."
        ),
    ] = False
    webhook_activity_limit: Annotated[
        int | None,
        Field(
            description="Maximum number of webhook delivery records to return per media buy, ordered most-recent first. Ignored when `include_webhook_activity` is false. Sellers that surface webhook activity MUST retain records for at least 30 days from each record's `completed_at` (see `webhook_activity` description in the response schema for the `pending`-status carve-out); sellers unable to honor that floor MUST omit the field entirely rather than truncate. When a buy has more historical fires than the limit, only the most recent are returned — there is no cursor for older fires; this surface is a debug aid, not a full audit log.",
            ge=1,
            le=200,
        ),
    ] = 50
    pagination: Annotated[
        pagination_request.PaginationRequest | None,
        Field(
            description='Cursor-based pagination controls. Strongly recommended when querying broad scopes (for example, all active media buys in an account).'
        ),
    ] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var account : adcp.types.generated_poc.core.account_ref.AccountReference | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var include_history : int | None
var include_snapshot : bool | None
var include_webhook_activity : bool | None
var indicator_types : list[adcp.types.generated_poc.enums.indicator_type.IndicatorType] | None
var media_buy_ids : list[str] | None
var model_config
var pagination : adcp.types.generated_poc.core.pagination_request.PaginationRequest | None
var status_filter : adcp.types.generated_poc.enums.media_buy_status.MediaBuyStatus | adcp.types.generated_poc.media_buy.get_media_buys_request.StatusFilter | None
var webhook_activity_limit : int | None

Inherited members

class GetMediaBuysResponse (**data: Any)

Base class enforcing the primary canonical runtime boundary.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var adcp_error : adcp.types.generated_poc.core.error.Error | None
var adcp_major_version : int | None
var adcp_version : str | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var context_id : str | None
var errors : list[adcp.types.generated_poc.core.error.Error] | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var governance_context : str | None
var media_buys : Sequence[MediaBuy]
var message : str | None
var model_config
var pagination : adcp.types.generated_poc.core.pagination_response.PaginationResponse | None
var payload : dict[str, typing.Any] | None
var push_notification_config : adcp.types.generated_poc.core.push_notification_config.PushNotificationConfig | None
var replayed : bool | None
var sandbox : bool | None
var status : adcp.types.generated_poc.enums.task_status.TaskStatus
var task_id : str | None
var timestamp : pydantic.types.AwareDatetime | None
class LegacyGetMediaBuysResponse (**data: Any)
Expand source code
class GetMediaBuysResponse(AdcpVersionEnvelope, ProtocolEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    media_buys: Annotated[
        Sequence[MediaBuy],
        Field(
            description='Array of media buys with status, creative approval state, and optional delivery snapshots'
        ),
    ]
    errors: Annotated[
        list[error.Error] | None,
        Field(
            description='Task-specific errors. A read may return media_buys plus nonfatal resource-state errors. If a pinned place target becomes unexecutable, sellers MUST include PLACE_TARGET_UNAVAILABLE with recovery=correctable, field pointing to the exact media_buys[N].packages[M].targeting_overlay.geo_places[_exclude][A].values[V] response path, and details containing media_buy_id, package_id, system, system_version, country, place_type, and value. The persisted target remains echoed until an intentional update replaces it.'
        ),
    ] = None
    pagination: Annotated[
        pagination_response.PaginationResponse | None,
        Field(description='Pagination metadata for the media_buys array.'),
    ] = None
    sandbox: Annotated[
        bool | None,
        Field(description='When true, this response contains simulated data from sandbox mode.'),
    ] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • adcp.types.generated_poc.core.protocol_envelope.ProtocolEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var errors : list[adcp.types.generated_poc.core.error.Error] | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var media_buys : Sequence[adcp.types.generated_poc.media_buy.get_media_buys_response.MediaBuy]
var model_config
var pagination : adcp.types.generated_poc.core.pagination_response.PaginationResponse | None
var sandbox : bool | None

Inherited members

class GetPlanAuditLogsRequest (**data: Any)
Expand source code
class GetPlanAuditLogsRequest(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    plan_ids: Annotated[
        list[str] | None,
        Field(
            description='Plan IDs to retrieve. For a single plan, pass a one-element array. Plans uniquely scope account and operator; do not include a separate `account` field — the governance agent resolves account from each plan. Including `account` is rejected by `additionalProperties: false`.',
            min_length=1,
        ),
    ] = None
    portfolio_plan_ids: Annotated[
        list[str] | None,
        Field(
            description='Portfolio plan IDs. The governance agent expands each to its member_plan_ids and returns combined audit data.',
            min_length=1,
        ),
    ] = None
    governance_contexts: Annotated[
        list[str] | None,
        Field(
            description='Filter audit entries by governance context. Returns only checks and outcomes that share these governance contexts, enabling lifecycle tracing across purchase types.',
            min_length=1,
        ),
    ] = None
    purchase_types: Annotated[
        list[purchase_type.PurchaseType] | None,
        Field(
            description="Filter audit entries by purchase type. Returns only checks and outcomes matching these purchase types (e.g., ['rights_license'] to see all rights activity).",
            min_length=1,
        ),
    ] = None
    include_entries: Annotated[
        bool | None, Field(description='Include the full audit trail. Default: false.')
    ] = False
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var governance_contexts : list[str] | None
var include_entries : bool | None
var model_config
var plan_ids : list[str] | None
var portfolio_plan_ids : list[str] | None
var purchase_types : list[adcp.types.generated_poc.enums.purchase_type.PurchaseType] | None

Inherited members

class GetPlanAuditLogsResponse (**data: Any)
Expand source code
class GetPlanAuditLogsResponse(AdcpVersionEnvelope, ProtocolEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    plans: Annotated[list[Plan], Field(description='Audit data for each requested plan.')]
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • adcp.types.generated_poc.core.protocol_envelope.ProtocolEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config
var plans : list[adcp.types.generated_poc.governance.get_plan_audit_logs_response.Plan]

Inherited members

class GetProductsRequest (**data: Any)
Expand source code
class GetProductsRequest(_GetProductsRequestBase):
    """Canonical discovery request with legacy response-field selection rejected."""

    @field_validator("fields")
    @classmethod
    def _reject_legacy_fields(cls, value: Any) -> Any:
        if value and any(
            is_legacy_creative_identity_key(getattr(item, "value", item)) for item in value
        ):
            raise ValueError(
                "format_id and format_ids are unavailable on the canonical get_products API"
            )
        return value

Canonical discovery request with legacy response-field selection rejected.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var model_config
class LegacyGetProductsRequest (**data: Any)
Expand source code
class GetProductsRequest(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    idempotency_key: Annotated[
        str | None,
        Field(
            description='Optional client-generated key for retry-safe use of the AdCP 3.x compatibility facade. New callers SHOULD use the compact 3.2 tasks; each stateful split task has its own idempotency identity, so callers MUST retry with the same tool name. Keys MUST be unique per seller and logical request.',
            max_length=255,
            min_length=16,
            pattern='^[A-Za-z0-9_.:-]{16,255}$',
        ),
    ] = None
    buying_mode: Annotated[
        BuyingMode,
        Field(
            description="Declares buyer intent for this request. 'brief': publisher curates product recommendations from the provided brief. 'wholesale': buyer requests raw product inventory to apply their own audiences — brief must not be provided, and proposals are omitted. 'refine': iterate on products and proposals from a previous get_products response using the refine array of change requests. v3 clients MUST include buying_mode. Sellers receiving requests from pre-v3 clients without buying_mode SHOULD default to 'brief'. Timing semantics: 'wholesale' is a wholesale product feed read — sellers SHOULD return a synchronous response and MUST NOT route a 'wholesale' request through the async/Submitted arm; partial completion is signalled via the response's incomplete[] field (with optional estimated_wait), not via a task-handoff envelope. 'brief' and 'refine' MAY complete synchronously, or MAY return a Submitted envelope (see get-products-async-response-submitted.json) when curation requires upstream-system queries or HITL review the seller cannot complete inside time_budget. Buyers needing predictable fast wholesale product feed access MUST use 'wholesale'; buyers open to slower curation use 'brief' or 'refine'."
        ),
    ]
    brief: Annotated[
        str | None,
        Field(
            description="Natural language description of campaign requirements. Required when buying_mode is 'brief'. Must not be provided when buying_mode is 'wholesale' or 'refine'. Buyers SHOULD use structured fields for every requirement that can be expressed structurally, and reserve brief prose for goals, context, preferences, and requirements without a structured representation. Sellers MUST apply explicit hard requirements stated in the brief even when the buyer did not duplicate them in a structured field. When a seller translates hard prose into structured targeting that materially affects product eligibility, pricing, or forecasting, it MUST confirm that interpretation once in GetProductsResponse.targeting_resolution.brief_targeting; otherwise confirmation remains a best practice. If hard prose contradicts a structured field, sellers MUST reject the request with INVALID_REQUEST rather than choose one interpretation or return an unexplained empty result."
        ),
    ] = None
    refine: Annotated[
        list[Refine] | None,
        Field(
            description="Array of change requests for iterating on products and proposals from a previous get_products response. Each entry declares a scope (request, product, or proposal) and what the buyer is asking for. Only valid when buying_mode is 'refine'. The seller responds to each entry via refinement_applied in the response, matched by position.\n\nFinalize-exclusivity rule: if any entry has `action: 'finalize'`, ALL entries in the array MUST be proposal-scoped with `action: 'finalize'` — mixing finalize entries with `include`/`omit` entries or with request- / product-scoped entries MUST be rejected by the seller with `INVALID_REQUEST`. Finalize is a commit, not a refinement; the buyer expressing intent to commit means refinements have already converged. Buyers needing to refine AND commit in close succession sequence the calls: first a refine call (no finalize), then a finalize call against the resulting `proposal_id`(s).\n\nMulti-finalize semantics: multiple finalize entries against different `proposal_id` values in a single call are allowed and MUST be **atomic at the observation point** — sellers MUST NOT return a success response unless every named proposal has both completed and been persisted as committed. Pre-commit validation runs before any side-effects (inventory pull, terms lock, governance attestation); if any proposal fails validation, the seller MUST reject the entire call without committing any of the named proposals. There is no rollback operation in the spec — an `unfinalize` would itself be a new mutation surface; the atomicity guarantee runs entirely on the seller's pre-commit validation gate, not on post-commit reversal. Sellers that cannot guarantee atomic pre-commit validation MUST reject multi-finalize arrays with `MULTI_FINALIZE_UNSUPPORTED` (preferred — distinguishes seller-side capability gap from a malformed request) or `INVALID_REQUEST` (acceptable fallback for sellers on a pre-3.1 error catalog). If a mid-commit failure occurs *after* validation passed but before all proposals persist (e.g., a downstream ad server fails between commits one and two), the seller MUST return `INTERNAL_ERROR` with `refinement_applied[]` carrying per-position outcomes — the spec does NOT define a recovery path for this case, and buyers SHOULD treat the resulting state as undefined and re-read via `get_media_buys` / equivalent before retrying. Buyers MUST NOT assume multi-finalize support without a successful first attempt — there is no capability flag for this; the failure response is the discovery surface. Buyers whose intent specifically requires atomic commit (e.g., budget-shared proposals where one finalizing without the other is incoherent) MUST be prepared to abandon the intent if the seller returns `MULTI_FINALIZE_UNSUPPORTED` — there is no recovery for that loss of buyer intent beyond sequencing single-finalize calls and accepting the looser commit guarantee.",
            min_length=1,
        ),
    ] = None
    brand: Annotated[
        brand_ref.BrandReference | None,
        Field(
            description='Brand reference for product discovery context. Resolved to full brand identity at execution time.'
        ),
    ] = None
    catalog: Annotated[
        catalog_1.Catalog | None,
        Field(
            description='Catalog of items the buyer wants to promote. The seller matches catalog items against its inventory and returns products where matches exist. Supports all catalog types: a job catalog finds job ad products, a product catalog finds sponsored product slots. Reference a synced catalog by catalog_id, or provide inline items.'
        ),
    ] = None
    account: Annotated[
        account_ref.AccountReference | None,
        Field(
            description="Account for product lookup. Returns products with pricing specific to this account's rate card."
        ),
    ] = None
    preferred_delivery_types: Annotated[
        list[delivery_type_1.DeliveryType] | None,
        Field(
            description='Delivery types the buyer prefers, in priority order. Unlike filters.delivery_type which excludes non-matching products, this signals preference for curation — the publisher may still include other delivery types when they match the brief well.',
            min_length=1,
        ),
    ] = None
    filters: Annotated[
        product_filters.ProductFilters | None,
        Field(
            description="Offer filters. Valid in brief, wholesale, and refine modes. In every mode, sellers MUST exclude products that do not satisfy them: brief controls curation, wholesale controls feed behavior, and refine controls iteration, but none changes filter semantics. On refine, presence is the complete replacement filter state; when omitted, each referenced product's bound discovery constraints remain in force. Targeting-like legacy fields remain accepted during migration but are deprecated in favor of targeting_overlay and required_overlay_support."
        ),
    ] = None
    targeting_overlay: Annotated[
        targeting.TargetingOverlay | None,
        Field(
            description="Concrete delivery constraints the buyer expects to carry into create_media_buy. Buyers SHOULD use this field instead of putting equivalent exact targeting only in brief prose. Sellers evaluate these constraints during discovery and scope returned pricing and forecasts to the effective targeting. If a product cannot honor the request exactly, the seller may omit it or return a request-scoped configured product with Product targeting_resolution modifications. Absence of Product targeting_resolution means exact acceptance of this structured overlay; it does not confirm how targeting in the brief was interpreted. On refine, presence is complete replacement state for returned configurations; when omitted, each referenced product's bound targeting remains in force."
        ),
    ] = None
    required_overlay_support: Annotated[
        targeting_overlay_requirements.TargetingOverlayRequirements | None,
        Field(
            description="Minimum product-scoped targeting dimensions the buyer must be able to set independently on packages later. This requests selectable capability, not current targeting values, value-specific availability or forecasts, and not one product per possible value. A requirement true matches support true or any valid support object; an object requirement matches support true or an object containing every requested boolean and requested array subset. Unrequested object fields and numeric limits do not participate. Missing or unknown requirement fields do not match. Seller limit fields are response-only and cannot be requested here. On refine, presence is complete replacement state; when omitted, each referenced product's bound future-support requirements remain in force."
        ),
    ] = None
    property_list: Annotated[
        property_list_ref.PropertyListReference | None,
        Field(
            deprecated=True,
            description='DEPRECATED discovery-only property filter. Use targeting_overlay.property_list when the list is a concrete delivery constraint, or required_overlay_support.property_list when the list will be supplied later.',
        ),
    ] = None
    fields: Annotated[
        list[Field1] | None,
        Field(
            description='Specific product fields to include in the response. When omitted, all fields are returned. Use for lightweight discovery calls where only a subset of product data is needed. product_id and name are always included. `format_ids` is a deprecated 3.x compatibility projection; new integrations request canonical `format_options`. Safety-critical request-specific fields override projection: Product.targeting_resolution and expires_at MUST be included whenever the seller returns modifications, overlay_support MUST be included when required_overlay_support was requested, and audience_evidence_selections MUST be included when filters.audience_evidence_requirements affects eligibility or ranking. fields controls the optional audience_evidence payload, not the evidence decision receipt. Response-level brief targeting confirmation is not a projected product field.',
            min_length=1,
        ),
    ] = None
    time_budget: Annotated[
        duration.Duration | None,
        Field(
            description='Maximum time the buyer will commit to this request. The seller returns the best results achievable within this budget and does not start processes (human approvals, expensive external queries) that cannot complete in time. When omitted, the seller decides timing.'
        ),
    ] = None
    push_notification_config: Annotated[
        push_notification_config_1.PushNotificationConfig | None,
        Field(
            description='Optional webhook configuration for async terminal completion/failure notifications on curated discovery. Meaningful only for `buying_mode: "brief"` and `buying_mode: "refine"` requests that enter the async lifecycle. Submitted envelopes with `task_id` remain pollable through `get_task_status` (legacy `tasks/get`) whether or not this field is present. If a brief/refine request includes this field and the seller returns a Submitted envelope, the seller MUST deliver at least the terminal completion/failure notification to the configured URL; intermediate progress notifications are MAY. If the seller cannot honor the webhook channel, it MUST reject the request with a structured error instead of silently accepting. This field does not change wholesale timing semantics: sellers MUST NOT route `buying_mode: "wholesale"` requests through the async/Submitted arm or emit async delivery solely because `push_notification_config` is present; partial wholesale completion is reported via `incomplete[]`.'
        ),
    ] = None
    pagination: Annotated[
        pagination_request.PaginationRequest | None,
        Field(
            description="Cursor-based pagination controls for get_products. Valid in all buying modes. In brief mode, pagination bounds the seller's returned products[] for the curated answer to the brief and is not an exhaustive catalog-enumeration contract. In refine mode, pagination bounds the refined products[] result implied by refine[] and filters; proposals may accompany a page as plan metadata but are not independently counted by this pagination envelope. In wholesale mode, pagination walks the wholesale product feed and may be combined with wholesale feed versioning."
        ),
    ] = None
    if_wholesale_feed_version: Annotated[
        str | None,
        Field(
            description="Opaque wholesale_feed_version token returned by a prior wholesale-mode get_products response from this agent. Only valid when buying_mode is wholesale. When provided, the seller compares against its current wholesale product feed version for the buyer's cache_scope and MAY return an unchanged: true response (with products omitted) if nothing has changed. The token is scope-keyed: buyers cache `(cache_scope, wholesale_feed_version)` pairs. Scoping dimensions: (agent, buying_mode, filters, targeting_overlay, required_overlay_support, deprecated property_list, catalog) for cache_scope: 'public'; that tuple plus account identity for cache_scope: 'account'. pagination.cursor is NOT part of the scoping tuple. Backward-compatible: pre-v3.1 agents that ignore this field simply return the full payload, same as the unchanged-server path. See specs/wholesale-feed-webhooks.md for the full sync pattern."
        ),
    ] = None
    if_pricing_version: Annotated[
        str | None,
        Field(
            description="Opaque pricing_version token from a prior get_products response. MUST only be sent together with if_wholesale_feed_version — pricing version has no structural baseline to compare against on its own. Evaluation order: (1) if_wholesale_feed_version mismatch → seller returns the full payload (pricing is implicitly stale); (2) if_wholesale_feed_version matches but if_pricing_version mismatches → seller returns the full payload so the buyer sees updated pricing_options; (3) both match → seller MAY return unchanged: true. Agents that don't track pricing separately ignore if_pricing_version and fall back to if_wholesale_feed_version semantics. Useful for storefronts that re-price compositions far more often than they re-render product mirrors."
        ),
    ] = None
    context: context_1.ContextObject | None = None
    required_policies: Annotated[
        list[str] | None,
        Field(
            description='Registry policy IDs that the buyer requires to be enforced for products in this response. Sellers filter products to only those that comply with or already enforce the requested policies.'
        ),
    ] = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var account : adcp.types.generated_poc.core.account_ref.AccountReference | None
var brand : adcp.types.generated_poc.core.brand_ref.BrandReference | None
var brief : str | None
var buying_mode : adcp.types.generated_poc.media_buy.get_products_request.BuyingMode | None
var catalog : adcp.types.generated_poc.core.catalog.Catalog | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var fields : list[adcp.types.generated_poc.media_buy.get_products_request.Field1] | None
var filters : adcp.types.generated_poc.core.product_filters.ProductFilters | None
var idempotency_key : str | None
var if_pricing_version : str | None
var if_wholesale_feed_version : str | None
var model_config
var pagination : adcp.types.generated_poc.core.pagination_request.PaginationRequest | None
var preferred_delivery_types : list[adcp.types.generated_poc.enums.delivery_type.DeliveryType] | None
var push_notification_config : adcp.types.generated_poc.core.push_notification_config.PushNotificationConfig | None
var refine : list[adcp.types.generated_poc.media_buy.get_products_request.Refine] | None
var required_overlay_support : adcp.types.generated_poc.core.targeting_overlay_requirements.TargetingOverlayRequirements | None
var required_policies : list[str] | None
var targeting_overlay : adcp.types.generated_poc.core.targeting.TargetingOverlay | None
var time_budget : adcp.types.generated_poc.core.duration.Duration | None

Instance variables

var adcp_major_version : int | None
Expand source code
def __get__(self, obj: BaseModel | None, obj_type: type[BaseModel] | None = None) -> Any:
    if obj is None:
        if self.wrapped_property is not None:
            return self.wrapped_property.__get__(None, obj_type)
        raise AttributeError(self.field_name)

    warnings.warn(self.msg, DeprecationWarning, stacklevel=2)

    if self.wrapped_property is not None:
        return self.wrapped_property.__get__(obj, obj_type)
    return obj.__dict__[self.field_name]

Read-only data descriptor used to emit a runtime deprecation warning before accessing a deprecated field.

Attributes
-----=
msg
The deprecation message to be emitted.
wrapped_property
The property instance if the deprecated field is a computed field, or None.
field_name
The name of the field being deprecated.
var property_list : adcp.types.generated_poc.core.property_list_ref.PropertyListReference | None
Expand source code
def __get__(self, obj: BaseModel | None, obj_type: type[BaseModel] | None = None) -> Any:
    if obj is None:
        if self.wrapped_property is not None:
            return self.wrapped_property.__get__(None, obj_type)
        raise AttributeError(self.field_name)

    warnings.warn(self.msg, DeprecationWarning, stacklevel=2)

    if self.wrapped_property is not None:
        return self.wrapped_property.__get__(obj, obj_type)
    return obj.__dict__[self.field_name]

Read-only data descriptor used to emit a runtime deprecation warning before accessing a deprecated field.

Attributes
-----=
msg
The deprecation message to be emitted.
wrapped_property
The property instance if the deprecated field is a computed field, or None.
field_name
The name of the field being deprecated.
class GetProductsBriefRequest (**data: Any)
Expand source code
class GetProductsRequest(_GetProductsRequestBase):
    """Canonical discovery request with legacy response-field selection rejected."""

    @field_validator("fields")
    @classmethod
    def _reject_legacy_fields(cls, value: Any) -> Any:
        if value and any(
            is_legacy_creative_identity_key(getattr(item, "value", item)) for item in value
        ):
            raise ValueError(
                "format_id and format_ids are unavailable on the canonical get_products API"
            )
        return value

Canonical discovery request with legacy response-field selection rejected.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var model_config
class GetProductsRefineRequest (**data: Any)
Expand source code
class GetProductsRequest(_GetProductsRequestBase):
    """Canonical discovery request with legacy response-field selection rejected."""

    @field_validator("fields")
    @classmethod
    def _reject_legacy_fields(cls, value: Any) -> Any:
        if value and any(
            is_legacy_creative_identity_key(getattr(item, "value", item)) for item in value
        ):
            raise ValueError(
                "format_id and format_ids are unavailable on the canonical get_products API"
            )
        return value

Canonical discovery request with legacy response-field selection rejected.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var model_config
class GetProductsWholesaleRequest (**data: Any)
Expand source code
class GetProductsRequest(_GetProductsRequestBase):
    """Canonical discovery request with legacy response-field selection rejected."""

    @field_validator("fields")
    @classmethod
    def _reject_legacy_fields(cls, value: Any) -> Any:
        if value and any(
            is_legacy_creative_identity_key(getattr(item, "value", item)) for item in value
        ):
            raise ValueError(
                "format_id and format_ids are unavailable on the canonical get_products API"
            )
        return value

Canonical discovery request with legacy response-field selection rejected.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var model_config

Inherited members

class GetProductsResponse (**data: Any)

Base class enforcing the primary canonical runtime boundary.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var adcp_error : adcp.types.generated_poc.core.error.Error | None
var adcp_major_version : int | None
var adcp_version : str | None
var cache_scope : adcp.types.generated_poc.media_buy.get_products_response.CacheScope | None
var catalog_applied : bool | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var context_id : str | None
var errors : list[adcp.types.generated_poc.core.error.Error] | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var extensions : dict[str, adcp.types.generated_poc.media_buy.get_products_response.Extensions] | None
var filter_diagnostics : adcp.types.generated_poc.media_buy.get_products_response.FilterDiagnostics | None
var governance_context : str | None
var incomplete : list[adcp.types.generated_poc.media_buy.get_products_response.IncompleteItem] | None
var message : str | None
var model_config
var pagination : adcp.types.generated_poc.core.pagination_response.PaginationResponse | None
var payload : dict[str, typing.Any] | None
var pricing_version : str | None
var products : list[Product] | None
var property_list_applied : bool | None
var proposals : list[adcp.types.generated_poc.core.proposal.Proposal] | None
var push_notification_config : adcp.types.generated_poc.core.push_notification_config.PushNotificationConfig | None
var reason : str | None
var refinement_applied : list[adcp.types.generated_poc.media_buy.get_products_response.RefinementApplied] | None
var replayed : bool | None
var sandbox : bool | None
var status : adcp.types.generated_poc.enums.task_status.TaskStatus
var suggestions : list[adcp.types.generated_poc.media_buy.get_products_response.Suggestion] | None
var targeting_resolution : adcp.types.generated_poc.media_buy.get_products_targeting_resolution.ProductDiscoveryTargetingResolution | None
var task_id : str | None
var timestamp : pydantic.types.AwareDatetime | None
var unchanged : Literal[True] | None
var wholesale_feed_version : str | None
class LegacyGetProductsResponse (**data: Any)
Expand source code
class GetProductsResponse(AdcpVersionEnvelope, ProtocolEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    products: Annotated[
        list[product.Product] | None, Field(description='Array of matching products')
    ] = None
    targeting_resolution: Annotated[
        get_products_targeting_resolution.ProductDiscoveryTargetingResolution | None,
        Field(
            description='Request-level confirmation of structured hard targeting inferred from the brief. Sellers MUST include this when their structured interpretation of hard prose materially affects product eligibility, pricing, or forecasting; otherwise inclusion is a best practice. Omitted when no hard targeting was inferred from the brief.'
        ),
    ] = None
    extensions: Annotated[
        dict[Annotated[str, StringConstraints(pattern=r'^https?://[^@]+@sha256:[a-f0-9]{64}$')], Extensions] | None,
        Field(
            description='Bundled platform-extension definitions referenced by any product in `products`. Keyed by `<extension_uri>@<digest>` (e.g., `https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel@sha256:abc...`). When present, lets buyers resolve `platform_extensions` references on product format declarations without a separate fetch. Buyer SDKs cache by URI@digest; subsequent get_products responses MAY omit definitions the buyer already has cached and rely on the digest match. Each value is an extension definition with `extends` (the canonical concept it extends, e.g., `tracking`), `fields` (the schema for additional fields the extension contributes), `version`, and optional `description`.'
        ),
    ] = None
    proposals: Annotated[
        list[proposal.Proposal] | None,
        Field(
            description='Optional array of proposed media plans with budget allocations across products. Publishers include proposals when they can provide strategic guidance based on the brief. Proposals are actionable - buyers can refine them via follow-up get_products calls within the same session, or execute them directly via create_media_buy.'
        ),
    ] = None
    errors: Annotated[
        list[error.Error] | None,
        Field(description='Task-specific errors and warnings (e.g., product filtering issues)'),
    ] = None
    reason: Annotated[
        str | None,
        Field(
            description='Buyer-facing market explanation required only on the GetProductsRejected arm. MAY be sanitized to protect confidential seller rules. Plain text only.',
            max_length=2000,
            min_length=1,
        ),
    ] = None
    suggestions: Annotated[
        list[Suggestion] | None,
        Field(
            description='Actionable alternatives available only on the GetProductsRejected arm.',
            max_length=20,
        ),
    ] = None
    property_list_applied: Annotated[
        bool | None,
        Field(
            description='[AdCP 3.0] Indicates whether property_list filtering was applied. True if the agent filtered products based on the provided property_list. Absent or false if property_list was not provided or not supported by this agent.'
        ),
    ] = None
    catalog_applied: Annotated[
        bool | None,
        Field(
            description='Whether the seller filtered results based on the provided catalog. True if the seller matched catalog items against its inventory. Absent or false if no catalog was provided or the seller does not support catalog matching.'
        ),
    ] = None
    refinement_applied: Annotated[
        list[RefinementApplied] | None,
        Field(
            description="Seller's response to each change request in the refine array, matched by position. Each entry acknowledges whether the corresponding ask was applied, partially applied, or unable to be fulfilled. MUST contain the same number of entries in the same order as the request's refine array. Only present when the request used buying_mode: 'refine'. Each entry MUST echo the request entry's scope and — for product and proposal scopes — the matching id field (product_id or proposal_id), so orchestrators can cross-validate alignment."
        ),
    ] = None
    incomplete: Annotated[
        list[IncompleteItem] | None,
        Field(
            description="Declares what the seller could not finish within the buyer's time_budget or due to internal limits while still returning a usable response. Each entry identifies a scope that is missing or partial. Absent when the response is fully complete. This field does not classify the condition as retryable; retryability is carried by error.recovery on the error channel.",
            min_length=1,
        ),
    ] = None
    filter_diagnostics: Annotated[
        FilterDiagnostics | None,
        Field(
            description="Optional non-fatal diagnostic block describing how the request's `filters` narrowed the candidate set. Use this to disambiguate empty/small result lists between 'no inventory matches the brief' and 'a specific filter excluded everything', without breaking the filter-not-fail convention (sellers still silently exclude unmatched products; this block is observability, not error reporting). Sellers MAY populate this when meaningful narrowing occurred; buyers MAY use it for triage UX without depending on its presence. Counts only — products are not enumerated by name to avoid leaking competitive intelligence about adjacent campaigns or seller inventory. `total_candidates` and `excluded_by` are independently optional — sellers whose baseline candidate set size is sensitive MAY emit `excluded_by` without `total_candidates`, or vice versa.",
            examples=[
                {
                    'semantics': 'only',
                    'total_candidates': 47,
                    'excluded_by': {
                        'required_metrics': {'count': 31, 'values': ['completed_views']},
                        'required_geo_targeting': {'count': 9},
                        'pricing_currencies': {'count': 3, 'values': ['USD']},
                        'budget_range': {'count': 7},
                    },
                }
            ],
        ),
    ] = None
    pagination: Annotated[
        pagination_response.PaginationResponse | None,
        Field(
            description="Cursor metadata for paginated get_products responses. In brief/refine mode, continuation pages bound returned products[] for the seller's curated or refined answer; proposals may accompany a page as plan metadata but are not independently counted by this pagination envelope, and pagination does not convert the response into an exhaustive feed contract. In wholesale mode, continuation pages walk the wholesale product feed."
        ),
    ] = None
    wholesale_feed_version: Annotated[
        str | None,
        Field(
            description="Opaque token representing the version of the wholesale product feed state used to compose this response. Sellers that implement conditional-fetch (if_wholesale_feed_version) MUST return this on every wholesale-mode response so buyers can cache and probe later. Buyers MUST treat the value as opaque — no format, no ordering, no inspection. The token is scope-keyed: it describes a version for the cache_scope declared on this response, NOT a global agent version. A buyer caches `(cache_scope, wholesale_feed_version)` pairs and presents the matching token on the next request. Scoping dimensions: (agent, buying_mode, filters, targeting_overlay, required_overlay_support, deprecated property_list, catalog) for cache_scope: 'public'; that tuple plus account_id for cache_scope: 'account'. pagination.cursor is NOT part of the scoping tuple. See specs/wholesale-feed-webhooks.md for the full cache layering model."
        ),
    ] = None
    pricing_version: Annotated[
        str | None,
        Field(
            description='Opaque token representing the version of the pricing layer, including product pricing_options and nested signal_targeting_options pricing_options. When the seller supports independent pricing versioning, pricing_version changes when prices move but wholesale_feed_version changes only when structure/metadata moves. Same cache_scope keying as wholesale_feed_version. Sellers not separating these MAY omit pricing_version and use wholesale_feed_version for both.'
        ),
    ] = None
    cache_scope: Annotated[
        CacheScope | None,
        Field(
            description="Declares whether the wholesale_feed_version and pricing_version on this response describe a universal layer or an account-specific overlay. REQUIRED on every 3.1+ response (the 3.1 schema enforces this — the safety property of the two-layer cache model depends on it). 'public': this response describes the seller's published rate card; the buyer MAY dedupe under (agent, buying_mode, filters, targeting_overlay, required_overlay_support, deprecated property_list, catalog) without scoping by account. 'account': this response includes account-specific overrides; the buyer MUST cache the version under (agent, buying_mode, filters, targeting_overlay, required_overlay_support, deprecated property_list, catalog, account_id). When the request did NOT include `account`, the seller MUST return `cache_scope: 'public'`. When the request included `account`, the seller MUST return either: 'public' (this account prices off the public rate card — buyer dedupes) or 'account' (account-specific overrides exist — buyer caches under the account key). Sellers MAY return 'public' on an account-scoped request that previously had overrides — buyers SHOULD interpret this as a downgrade and drop their account-overlay for the (agent, filters, targeting, support, mode) tuple. Without schema-required cache_scope, a seller silently omitting the field on an account-scoped response would cause buyers to mis-key the cache and serve account-overlay payloads to other accounts — the canonical safety invariant of the entire cache layering model. **Backward-compatibility note for 3.1 validators:** SDKs that validate strictly against the 3.1 schema MUST select the validator based on the server-declared `adcp_version` (release-precision version negotiation, 3.1). For responses with `adcp_version` starting `3.0`, the 3.1 cache_scope-required constraint MUST be relaxed — pre-3.1 sellers correctly emit no cache_scope and remain conformant to their declared version. This is a tightening within 3.1, not a 3.0 break."
        ),
    ] = CacheScope.public
    unchanged: Annotated[
        Literal[True] | None,
        Field(
            description="Present and `true` ONLY on wholesale-mode responses when the request carried if_wholesale_feed_version (and/or if_pricing_version) matching the seller's current version for the buyer's cache_scope, in which case products[] MUST be omitted; wholesale_feed_version (echoed), cache_scope (echoed), and pricing_version (echoed when used) MUST still be present. Buyers receiving unchanged: true MUST NOT mutate their local wholesale product mirror. **One shape per state:** sellers MUST NOT emit `unchanged: false` — the absence of the field IS the signal that the response carries products. Two shapes ({ unchanged: false, products: [...] } vs. { products: [...] }) for the same state would let some sellers always emit the field and some never would, creating an inconsistency the wire shouldn't carry. **Cross-scope isolation:** the comparator that decides `unchanged` MUST be keyed on `(cache_scope, wholesale_feed_version)`, not on the token value alone. A seller MUST NOT emit `unchanged: true` when it resolves the request to a different `cache_scope` than the one whose token the buyer echoed in `if_wholesale_feed_version` (and/or `if_pricing_version`): because the token is scope-keyed, a value minted for `cache_scope: 'public'` cannot match the seller's current token for `cache_scope: 'account'` (or vice-versa), so such a request MUST return the full feed for the resolved scope with that scope's own token."
        ),
    ] = None
    sandbox: Annotated[
        bool | None,
        Field(description='When true, this response contains simulated data from sandbox mode.'),
    ] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • adcp.types.generated_poc.core.protocol_envelope.ProtocolEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var cache_scope : adcp.types.generated_poc.media_buy.get_products_response.CacheScope | None
var catalog_applied : bool | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var errors : list[adcp.types.generated_poc.core.error.Error] | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var extensions : dict[str, adcp.types.generated_poc.media_buy.get_products_response.Extensions] | None
var filter_diagnostics : adcp.types.generated_poc.media_buy.get_products_response.FilterDiagnostics | None
var incomplete : list[adcp.types.generated_poc.media_buy.get_products_response.IncompleteItem] | None
var model_config
var pagination : adcp.types.generated_poc.core.pagination_response.PaginationResponse | None
var pricing_version : str | None
var products : list[adcp.types.generated_poc.core.product.Product] | None
var property_list_applied : bool | None
var proposals : list[adcp.types.generated_poc.core.proposal.Proposal] | None
var reason : str | None
var refinement_applied : list[adcp.types.generated_poc.media_buy.get_products_response.RefinementApplied] | None
var sandbox : bool | None
var status : adcp.types.generated_poc.enums.task_status.TaskStatus | None
var suggestions : list[adcp.types.generated_poc.media_buy.get_products_response.Suggestion] | None
var targeting_resolution : adcp.types.generated_poc.media_buy.get_products_targeting_resolution.ProductDiscoveryTargetingResolution | None
var unchanged : Literal[True] | None
var wholesale_feed_version : str | None

Instance variables

var adcp_major_version : int | None
Expand source code
def __get__(self, obj: BaseModel | None, obj_type: type[BaseModel] | None = None) -> Any:
    if obj is None:
        if self.wrapped_property is not None:
            return self.wrapped_property.__get__(None, obj_type)
        raise AttributeError(self.field_name)

    warnings.warn(self.msg, DeprecationWarning, stacklevel=2)

    if self.wrapped_property is not None:
        return self.wrapped_property.__get__(obj, obj_type)
    return obj.__dict__[self.field_name]

Read-only data descriptor used to emit a runtime deprecation warning before accessing a deprecated field.

Attributes
-----=
msg
The deprecation message to be emitted.
wrapped_property
The property instance if the deprecated field is a computed field, or None.
field_name
The name of the field being deprecated.

Inherited members

class GetRightsRequest (**data: Any)
Expand source code
class GetRightsRequest(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    query: Annotated[
        str,
        Field(
            description='Natural language description of desired rights. The agent interprets intent, budget signals, and compatibility from this text.',
            max_length=2000,
        ),
    ]
    uses: Annotated[
        list[right_use.RightUse],
        Field(
            description='Rights uses being requested. The agent returns options covering these uses, potentially bundled into composite pricing.',
            min_length=1,
        ),
    ]
    buyer_brand: Annotated[
        brand_ref.BrandReference | None,
        Field(
            description="The buyer's brand. The agent fetches the buyer's brand.json for compatibility filtering (e.g., dietary conflicts, competitor exclusions)."
        ),
    ] = None
    countries: Annotated[
        list[Country] | None,
        Field(
            description='Countries where rights are needed (ISO 3166-1 alpha-2). Filters to rights available in these markets.'
        ),
    ] = None
    brand_id: Annotated[
        str | None,
        Field(
            description="Search within a specific brand's rights. If omitted, searches across the agent's full roster."
        ),
    ] = None
    right_type: Annotated[
        right_type_1.RightType | None,
        Field(description='Filter by type of rights (talent, music, stock_media, etc.)'),
    ] = None
    include_excluded: Annotated[
        bool | None,
        Field(
            description='Include filtered-out results in the excluded array with reasons. Defaults to false.'
        ),
    ] = False
    pagination: Annotated[
        pagination_request.PaginationRequest | None,
        Field(description='Pagination parameters for large result sets'),
    ] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var brand_id : str | None
var buyer_brand : adcp.types.generated_poc.core.brand_ref.BrandReference | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var countries : list[adcp.types.generated_poc.brand.get_rights_request.Country] | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var include_excluded : bool | None
var model_config
var pagination : adcp.types.generated_poc.core.pagination_request.PaginationRequest | None
var query : str
var right_type : adcp.types.generated_poc.enums.right_type.RightType | None
var uses : list[adcp.types.generated_poc.enums.right_use.RightUse]

Inherited members

class GetRightsResponse1 (**data: Any)
Expand source code
class GetRightsResponse1(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    rights: list[Right]
    excluded: list[Excluded] | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var excluded : list[adcp.types.generated_poc.brand.get_rights_response.Excluded] | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config
var rights : list[adcp.types.generated_poc.brand.get_rights_response.Right]
class GetRightsSuccessResponse (**data: Any)
Expand source code
class GetRightsResponse1(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    rights: list[Right]
    excluded: list[Excluded] | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var excluded : list[adcp.types.generated_poc.brand.get_rights_response.Excluded] | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config
var rights : list[adcp.types.generated_poc.brand.get_rights_response.Right]

Inherited members

class GetRightsErrorResponse (**data: Any)
Expand source code
class GetRightsResponse2(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    errors: Annotated[list[error_1.Error], Field(min_length=1)]
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var errors : list[adcp.types.generated_poc.core.error.Error]
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config

Inherited members

class GetSignalsRequest (**data: Any)
Expand source code
class GetSignalsRequest(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    discovery_mode: Annotated[
        DiscoveryMode | None,
        Field(
            description="Declares caller intent for this request. 'brief' (default): semantic discovery — signal_spec, signal_refs, or legacy signal_ids is required and the agent performs inference/RAG. 'wholesale': raw wholesale signals feed enumeration — signal_spec, signal_refs, and signal_ids MUST NOT be provided and the agent returns its full priced signals feed, paginated, scoped by filters/account/destinations/countries when present. Sellers receiving requests from pre-v3.1 clients without discovery_mode MUST default to 'brief'. Timing semantics: 'wholesale' is a wholesale signals feed read — agents SHOULD respond synchronously and MUST NOT route a 'wholesale' request through the async/Submitted arm; partial completion is signalled via the response's incomplete[] field, not via a task-handoff envelope. Agents that do not implement wholesale enumeration MAY return INVALID_REQUEST for wholesale calls; callers SHOULD probe via get_adcp_capabilities (signals.discovery_modes) first."
        ),
    ] = DiscoveryMode.brief
    account: Annotated[
        account_ref.AccountReference | None,
        Field(
            description="Account for this request. When provided, the signals agent returns per-account pricing options if configured. In 'wholesale' mode, this is the rate-card scope: when omitted in wholesale mode, agents return their default rate-card pricing or omit pricing_options entirely."
        ),
    ] = None
    signal_spec: Annotated[
        str | None,
        Field(
            description="Natural language description of the desired signals. When used alone, enables semantic discovery. When combined with signal_refs, provides context for the agent but signal_ref matches are returned first. MUST NOT be provided when discovery_mode is 'wholesale'."
        ),
    ] = None
    signal_refs: Annotated[
        list[signal_ref_1.SignalRef] | None,
        Field(
            description="Specific signals to look up by reference. Returns exact matches for the requested SignalRef values. When combined with signal_spec, these signals anchor the starting set and signal_spec guides adjustments. MUST NOT be provided when discovery_mode is 'wholesale'.",
            min_length=1,
        ),
    ] = None
    signal_ids: Annotated[
        list[signal_id_1.SignalId] | None,
        Field(
            deprecated=True,
            description="DEPRECATED. Use signal_refs instead. Legacy exact lookup field using SignalId objects. MUST NOT be provided when discovery_mode is 'wholesale'.",
            min_length=1,
        ),
    ] = None
    destinations: Annotated[
        list[destination.Destination] | None,
        Field(
            description='Filter signals to those activatable on specific agents/platforms. When omitted, returns all signals available on the current agent. If the authenticated caller matches one of these destinations, activation keys will be included in the response.',
            min_length=1,
        ),
    ] = None
    countries: Annotated[
        list[Country] | None,
        Field(
            description='Countries where signals will be used (ISO 3166-1 alpha-2 codes). When omitted, no geographic filter is applied.',
            min_length=1,
        ),
    ] = None
    filters: signal_filters.SignalFilters | None = None
    fields: Annotated[
        list[Field1] | None,
        Field(
            description="Specific signal fields to include in the response, aligned with get_products.fields. Required identity and activation fields such as signal_ref or signal_id, signal_agent_segment_id, name, description, signal_type, coverage_percentage, and deployments are always included when required by the response schema. Use for progressive disclosure of rich signal-definition metadata: request fields such as demographic_predicate, taxonomy, data_sources, methodology, segmentation_criteria, criteria_url, refresh_cadence, lookback_window, onboarder, modeling, audience_expansion, device_expansion, countries, consent_basis, restricted_attributes, policy_categories, art9_basis, data_subject_rights, and last_updated when the buyer needs them inline. Omit for the agent's default discovery projection. Agents SHOULD honor requested fields for exact lookup, refinement, small custom-signal result sets, and private/source-native signals when available. fields is a projection request, not an entitlement grant; agents MAY redact requested definition fields unless the caller is authorized for the underlying lineage, methodology, and rights-routing metadata. When demographic_predicate, consent_basis, or art9_basis is projected for another provider's signal, the value remains provider-declared signal-definition posture; sellers and federating agents MUST NOT substitute their own semantics or processing basis. For broad discovery and wholesale pages, agents MAY return compact pointers instead of inlining large resources, especially when provider-published definitions can be resolved from signal_ref, taxonomy.ref, criteria_url, disclosure_url, and validators such as resolved URL plus catalog_etag, HTTP ETag/Last-Modified, or taxonomy.etag.",
            min_length=1,
        ),
    ] = None
    max_results: Annotated[
        int | None,
        Field(
            deprecated=True,
            description='DEPRECATED: Use pagination.max_results instead. When both fields are present, agents MUST honor pagination.max_results. When only this field is present without a pagination envelope, agents SHOULD treat it as the page size subject to a maximum of 100 results. This field will be removed in AdCP 4.0.',
            ge=1,
        ),
    ] = None
    pagination: Annotated[
        pagination_request.PaginationRequest | None,
        Field(
            description='Pagination parameters. Use pagination.max_results (max: 100, default: 50) and pagination.cursor for cursor-based page walks. When the deprecated top-level max_results field is also present, pagination.max_results takes precedence.'
        ),
    ] = None
    push_notification_config: Annotated[
        push_notification_config_1.PushNotificationConfig | None,
        Field(
            description='Optional webhook configuration for async terminal completion/failure notifications on semantic signal discovery. Meaningful only for `discovery_mode: "brief"` requests that enter the async lifecycle. Submitted envelopes with `task_id` remain pollable through `get_task_status` (legacy `tasks/get`) whether or not this field is present. If a brief request includes this field and the agent returns a Submitted envelope, the agent MUST deliver at least the terminal completion/failure notification to the configured URL; intermediate progress notifications are MAY. If the agent cannot honor the webhook channel, it MUST reject the request with a structured error instead of silently accepting. This field does not change wholesale timing semantics: agents MUST NOT route `discovery_mode: "wholesale"` requests through the async/Submitted arm or emit async delivery solely because `push_notification_config` is present; partial wholesale completion is reported via `incomplete[]`.'
        ),
    ] = None
    if_wholesale_feed_version: Annotated[
        str | None,
        Field(
            description="Opaque wholesale_feed_version token returned by a prior wholesale-mode get_signals response from this agent. Only valid when discovery_mode is wholesale. When provided, the agent compares against its current wholesale signals feed version for the caller's cache_scope and MAY return an unchanged: true response (with signals omitted) if nothing has changed. The token is scope-keyed: callers cache `(cache_scope, wholesale_feed_version)` pairs. Scoping dimensions: (agent, discovery_mode, filters, destinations, countries) for cache_scope: 'public'; that tuple plus account_id for cache_scope: 'account'. pagination.cursor is NOT part of the scoping tuple. See specs/wholesale-feed-webhooks.md for the full sync pattern."
        ),
    ] = None
    if_pricing_version: Annotated[
        str | None,
        Field(
            description="Opaque pricing_version token from a prior get_signals response. MUST only be sent together with if_wholesale_feed_version — pricing version has no structural baseline to compare against on its own. Evaluation order: (1) if_wholesale_feed_version mismatch → agent returns the full payload; (2) if_wholesale_feed_version matches but if_pricing_version mismatches → agent returns the full payload so the caller sees updated pricing_options; (3) both match → agent MAY return unchanged: true. Agents that don't track pricing separately ignore this and fall back to if_wholesale_feed_version semantics."
        ),
    ] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var account : adcp.types.generated_poc.core.account_ref.AccountReference | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var countries : list[adcp.types.generated_poc.signals.get_signals_request.Country] | None
var destinations : list[adcp.types.generated_poc.core.destination.Destination] | None
var discovery_mode : adcp.types.generated_poc.signals.get_signals_request.DiscoveryMode | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var fields : list[adcp.types.generated_poc.signals.get_signals_request.Field1] | None
var filters : adcp.types.generated_poc.core.signal_filters.SignalFilters | None
var if_pricing_version : str | None
var if_wholesale_feed_version : str | None
var max_results : int | None
var model_config
var pagination : adcp.types.generated_poc.core.pagination_request.PaginationRequest | None
var push_notification_config : adcp.types.generated_poc.core.push_notification_config.PushNotificationConfig | None
var signal_ids : list[adcp.types.generated_poc.core.signal_id.SignalId] | None
var signal_refs : list[adcp.types.generated_poc.core.signal_ref.SignalRef] | None
var signal_spec : str | None
class GetSignalsDiscoveryRequest (**data: Any)
Expand source code
class GetSignalsRequest(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    discovery_mode: Annotated[
        DiscoveryMode | None,
        Field(
            description="Declares caller intent for this request. 'brief' (default): semantic discovery — signal_spec, signal_refs, or legacy signal_ids is required and the agent performs inference/RAG. 'wholesale': raw wholesale signals feed enumeration — signal_spec, signal_refs, and signal_ids MUST NOT be provided and the agent returns its full priced signals feed, paginated, scoped by filters/account/destinations/countries when present. Sellers receiving requests from pre-v3.1 clients without discovery_mode MUST default to 'brief'. Timing semantics: 'wholesale' is a wholesale signals feed read — agents SHOULD respond synchronously and MUST NOT route a 'wholesale' request through the async/Submitted arm; partial completion is signalled via the response's incomplete[] field, not via a task-handoff envelope. Agents that do not implement wholesale enumeration MAY return INVALID_REQUEST for wholesale calls; callers SHOULD probe via get_adcp_capabilities (signals.discovery_modes) first."
        ),
    ] = DiscoveryMode.brief
    account: Annotated[
        account_ref.AccountReference | None,
        Field(
            description="Account for this request. When provided, the signals agent returns per-account pricing options if configured. In 'wholesale' mode, this is the rate-card scope: when omitted in wholesale mode, agents return their default rate-card pricing or omit pricing_options entirely."
        ),
    ] = None
    signal_spec: Annotated[
        str | None,
        Field(
            description="Natural language description of the desired signals. When used alone, enables semantic discovery. When combined with signal_refs, provides context for the agent but signal_ref matches are returned first. MUST NOT be provided when discovery_mode is 'wholesale'."
        ),
    ] = None
    signal_refs: Annotated[
        list[signal_ref_1.SignalRef] | None,
        Field(
            description="Specific signals to look up by reference. Returns exact matches for the requested SignalRef values. When combined with signal_spec, these signals anchor the starting set and signal_spec guides adjustments. MUST NOT be provided when discovery_mode is 'wholesale'.",
            min_length=1,
        ),
    ] = None
    signal_ids: Annotated[
        list[signal_id_1.SignalId] | None,
        Field(
            deprecated=True,
            description="DEPRECATED. Use signal_refs instead. Legacy exact lookup field using SignalId objects. MUST NOT be provided when discovery_mode is 'wholesale'.",
            min_length=1,
        ),
    ] = None
    destinations: Annotated[
        list[destination.Destination] | None,
        Field(
            description='Filter signals to those activatable on specific agents/platforms. When omitted, returns all signals available on the current agent. If the authenticated caller matches one of these destinations, activation keys will be included in the response.',
            min_length=1,
        ),
    ] = None
    countries: Annotated[
        list[Country] | None,
        Field(
            description='Countries where signals will be used (ISO 3166-1 alpha-2 codes). When omitted, no geographic filter is applied.',
            min_length=1,
        ),
    ] = None
    filters: signal_filters.SignalFilters | None = None
    fields: Annotated[
        list[Field1] | None,
        Field(
            description="Specific signal fields to include in the response, aligned with get_products.fields. Required identity and activation fields such as signal_ref or signal_id, signal_agent_segment_id, name, description, signal_type, coverage_percentage, and deployments are always included when required by the response schema. Use for progressive disclosure of rich signal-definition metadata: request fields such as demographic_predicate, taxonomy, data_sources, methodology, segmentation_criteria, criteria_url, refresh_cadence, lookback_window, onboarder, modeling, audience_expansion, device_expansion, countries, consent_basis, restricted_attributes, policy_categories, art9_basis, data_subject_rights, and last_updated when the buyer needs them inline. Omit for the agent's default discovery projection. Agents SHOULD honor requested fields for exact lookup, refinement, small custom-signal result sets, and private/source-native signals when available. fields is a projection request, not an entitlement grant; agents MAY redact requested definition fields unless the caller is authorized for the underlying lineage, methodology, and rights-routing metadata. When demographic_predicate, consent_basis, or art9_basis is projected for another provider's signal, the value remains provider-declared signal-definition posture; sellers and federating agents MUST NOT substitute their own semantics or processing basis. For broad discovery and wholesale pages, agents MAY return compact pointers instead of inlining large resources, especially when provider-published definitions can be resolved from signal_ref, taxonomy.ref, criteria_url, disclosure_url, and validators such as resolved URL plus catalog_etag, HTTP ETag/Last-Modified, or taxonomy.etag.",
            min_length=1,
        ),
    ] = None
    max_results: Annotated[
        int | None,
        Field(
            deprecated=True,
            description='DEPRECATED: Use pagination.max_results instead. When both fields are present, agents MUST honor pagination.max_results. When only this field is present without a pagination envelope, agents SHOULD treat it as the page size subject to a maximum of 100 results. This field will be removed in AdCP 4.0.',
            ge=1,
        ),
    ] = None
    pagination: Annotated[
        pagination_request.PaginationRequest | None,
        Field(
            description='Pagination parameters. Use pagination.max_results (max: 100, default: 50) and pagination.cursor for cursor-based page walks. When the deprecated top-level max_results field is also present, pagination.max_results takes precedence.'
        ),
    ] = None
    push_notification_config: Annotated[
        push_notification_config_1.PushNotificationConfig | None,
        Field(
            description='Optional webhook configuration for async terminal completion/failure notifications on semantic signal discovery. Meaningful only for `discovery_mode: "brief"` requests that enter the async lifecycle. Submitted envelopes with `task_id` remain pollable through `get_task_status` (legacy `tasks/get`) whether or not this field is present. If a brief request includes this field and the agent returns a Submitted envelope, the agent MUST deliver at least the terminal completion/failure notification to the configured URL; intermediate progress notifications are MAY. If the agent cannot honor the webhook channel, it MUST reject the request with a structured error instead of silently accepting. This field does not change wholesale timing semantics: agents MUST NOT route `discovery_mode: "wholesale"` requests through the async/Submitted arm or emit async delivery solely because `push_notification_config` is present; partial wholesale completion is reported via `incomplete[]`.'
        ),
    ] = None
    if_wholesale_feed_version: Annotated[
        str | None,
        Field(
            description="Opaque wholesale_feed_version token returned by a prior wholesale-mode get_signals response from this agent. Only valid when discovery_mode is wholesale. When provided, the agent compares against its current wholesale signals feed version for the caller's cache_scope and MAY return an unchanged: true response (with signals omitted) if nothing has changed. The token is scope-keyed: callers cache `(cache_scope, wholesale_feed_version)` pairs. Scoping dimensions: (agent, discovery_mode, filters, destinations, countries) for cache_scope: 'public'; that tuple plus account_id for cache_scope: 'account'. pagination.cursor is NOT part of the scoping tuple. See specs/wholesale-feed-webhooks.md for the full sync pattern."
        ),
    ] = None
    if_pricing_version: Annotated[
        str | None,
        Field(
            description="Opaque pricing_version token from a prior get_signals response. MUST only be sent together with if_wholesale_feed_version — pricing version has no structural baseline to compare against on its own. Evaluation order: (1) if_wholesale_feed_version mismatch → agent returns the full payload; (2) if_wholesale_feed_version matches but if_pricing_version mismatches → agent returns the full payload so the caller sees updated pricing_options; (3) both match → agent MAY return unchanged: true. Agents that don't track pricing separately ignore this and fall back to if_wholesale_feed_version semantics."
        ),
    ] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var account : adcp.types.generated_poc.core.account_ref.AccountReference | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var countries : list[adcp.types.generated_poc.signals.get_signals_request.Country] | None
var destinations : list[adcp.types.generated_poc.core.destination.Destination] | None
var discovery_mode : adcp.types.generated_poc.signals.get_signals_request.DiscoveryMode | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var fields : list[adcp.types.generated_poc.signals.get_signals_request.Field1] | None
var filters : adcp.types.generated_poc.core.signal_filters.SignalFilters | None
var if_pricing_version : str | None
var if_wholesale_feed_version : str | None
var max_results : int | None
var model_config
var pagination : adcp.types.generated_poc.core.pagination_request.PaginationRequest | None
var push_notification_config : adcp.types.generated_poc.core.push_notification_config.PushNotificationConfig | None
var signal_ids : list[adcp.types.generated_poc.core.signal_id.SignalId] | None
var signal_refs : list[adcp.types.generated_poc.core.signal_ref.SignalRef] | None
var signal_spec : str | None
class GetSignalsLookupRequest (**data: Any)
Expand source code
class GetSignalsRequest(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    discovery_mode: Annotated[
        DiscoveryMode | None,
        Field(
            description="Declares caller intent for this request. 'brief' (default): semantic discovery — signal_spec, signal_refs, or legacy signal_ids is required and the agent performs inference/RAG. 'wholesale': raw wholesale signals feed enumeration — signal_spec, signal_refs, and signal_ids MUST NOT be provided and the agent returns its full priced signals feed, paginated, scoped by filters/account/destinations/countries when present. Sellers receiving requests from pre-v3.1 clients without discovery_mode MUST default to 'brief'. Timing semantics: 'wholesale' is a wholesale signals feed read — agents SHOULD respond synchronously and MUST NOT route a 'wholesale' request through the async/Submitted arm; partial completion is signalled via the response's incomplete[] field, not via a task-handoff envelope. Agents that do not implement wholesale enumeration MAY return INVALID_REQUEST for wholesale calls; callers SHOULD probe via get_adcp_capabilities (signals.discovery_modes) first."
        ),
    ] = DiscoveryMode.brief
    account: Annotated[
        account_ref.AccountReference | None,
        Field(
            description="Account for this request. When provided, the signals agent returns per-account pricing options if configured. In 'wholesale' mode, this is the rate-card scope: when omitted in wholesale mode, agents return their default rate-card pricing or omit pricing_options entirely."
        ),
    ] = None
    signal_spec: Annotated[
        str | None,
        Field(
            description="Natural language description of the desired signals. When used alone, enables semantic discovery. When combined with signal_refs, provides context for the agent but signal_ref matches are returned first. MUST NOT be provided when discovery_mode is 'wholesale'."
        ),
    ] = None
    signal_refs: Annotated[
        list[signal_ref_1.SignalRef] | None,
        Field(
            description="Specific signals to look up by reference. Returns exact matches for the requested SignalRef values. When combined with signal_spec, these signals anchor the starting set and signal_spec guides adjustments. MUST NOT be provided when discovery_mode is 'wholesale'.",
            min_length=1,
        ),
    ] = None
    signal_ids: Annotated[
        list[signal_id_1.SignalId] | None,
        Field(
            deprecated=True,
            description="DEPRECATED. Use signal_refs instead. Legacy exact lookup field using SignalId objects. MUST NOT be provided when discovery_mode is 'wholesale'.",
            min_length=1,
        ),
    ] = None
    destinations: Annotated[
        list[destination.Destination] | None,
        Field(
            description='Filter signals to those activatable on specific agents/platforms. When omitted, returns all signals available on the current agent. If the authenticated caller matches one of these destinations, activation keys will be included in the response.',
            min_length=1,
        ),
    ] = None
    countries: Annotated[
        list[Country] | None,
        Field(
            description='Countries where signals will be used (ISO 3166-1 alpha-2 codes). When omitted, no geographic filter is applied.',
            min_length=1,
        ),
    ] = None
    filters: signal_filters.SignalFilters | None = None
    fields: Annotated[
        list[Field1] | None,
        Field(
            description="Specific signal fields to include in the response, aligned with get_products.fields. Required identity and activation fields such as signal_ref or signal_id, signal_agent_segment_id, name, description, signal_type, coverage_percentage, and deployments are always included when required by the response schema. Use for progressive disclosure of rich signal-definition metadata: request fields such as demographic_predicate, taxonomy, data_sources, methodology, segmentation_criteria, criteria_url, refresh_cadence, lookback_window, onboarder, modeling, audience_expansion, device_expansion, countries, consent_basis, restricted_attributes, policy_categories, art9_basis, data_subject_rights, and last_updated when the buyer needs them inline. Omit for the agent's default discovery projection. Agents SHOULD honor requested fields for exact lookup, refinement, small custom-signal result sets, and private/source-native signals when available. fields is a projection request, not an entitlement grant; agents MAY redact requested definition fields unless the caller is authorized for the underlying lineage, methodology, and rights-routing metadata. When demographic_predicate, consent_basis, or art9_basis is projected for another provider's signal, the value remains provider-declared signal-definition posture; sellers and federating agents MUST NOT substitute their own semantics or processing basis. For broad discovery and wholesale pages, agents MAY return compact pointers instead of inlining large resources, especially when provider-published definitions can be resolved from signal_ref, taxonomy.ref, criteria_url, disclosure_url, and validators such as resolved URL plus catalog_etag, HTTP ETag/Last-Modified, or taxonomy.etag.",
            min_length=1,
        ),
    ] = None
    max_results: Annotated[
        int | None,
        Field(
            deprecated=True,
            description='DEPRECATED: Use pagination.max_results instead. When both fields are present, agents MUST honor pagination.max_results. When only this field is present without a pagination envelope, agents SHOULD treat it as the page size subject to a maximum of 100 results. This field will be removed in AdCP 4.0.',
            ge=1,
        ),
    ] = None
    pagination: Annotated[
        pagination_request.PaginationRequest | None,
        Field(
            description='Pagination parameters. Use pagination.max_results (max: 100, default: 50) and pagination.cursor for cursor-based page walks. When the deprecated top-level max_results field is also present, pagination.max_results takes precedence.'
        ),
    ] = None
    push_notification_config: Annotated[
        push_notification_config_1.PushNotificationConfig | None,
        Field(
            description='Optional webhook configuration for async terminal completion/failure notifications on semantic signal discovery. Meaningful only for `discovery_mode: "brief"` requests that enter the async lifecycle. Submitted envelopes with `task_id` remain pollable through `get_task_status` (legacy `tasks/get`) whether or not this field is present. If a brief request includes this field and the agent returns a Submitted envelope, the agent MUST deliver at least the terminal completion/failure notification to the configured URL; intermediate progress notifications are MAY. If the agent cannot honor the webhook channel, it MUST reject the request with a structured error instead of silently accepting. This field does not change wholesale timing semantics: agents MUST NOT route `discovery_mode: "wholesale"` requests through the async/Submitted arm or emit async delivery solely because `push_notification_config` is present; partial wholesale completion is reported via `incomplete[]`.'
        ),
    ] = None
    if_wholesale_feed_version: Annotated[
        str | None,
        Field(
            description="Opaque wholesale_feed_version token returned by a prior wholesale-mode get_signals response from this agent. Only valid when discovery_mode is wholesale. When provided, the agent compares against its current wholesale signals feed version for the caller's cache_scope and MAY return an unchanged: true response (with signals omitted) if nothing has changed. The token is scope-keyed: callers cache `(cache_scope, wholesale_feed_version)` pairs. Scoping dimensions: (agent, discovery_mode, filters, destinations, countries) for cache_scope: 'public'; that tuple plus account_id for cache_scope: 'account'. pagination.cursor is NOT part of the scoping tuple. See specs/wholesale-feed-webhooks.md for the full sync pattern."
        ),
    ] = None
    if_pricing_version: Annotated[
        str | None,
        Field(
            description="Opaque pricing_version token from a prior get_signals response. MUST only be sent together with if_wholesale_feed_version — pricing version has no structural baseline to compare against on its own. Evaluation order: (1) if_wholesale_feed_version mismatch → agent returns the full payload; (2) if_wholesale_feed_version matches but if_pricing_version mismatches → agent returns the full payload so the caller sees updated pricing_options; (3) both match → agent MAY return unchanged: true. Agents that don't track pricing separately ignore this and fall back to if_wholesale_feed_version semantics."
        ),
    ] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var account : adcp.types.generated_poc.core.account_ref.AccountReference | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var countries : list[adcp.types.generated_poc.signals.get_signals_request.Country] | None
var destinations : list[adcp.types.generated_poc.core.destination.Destination] | None
var discovery_mode : adcp.types.generated_poc.signals.get_signals_request.DiscoveryMode | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var fields : list[adcp.types.generated_poc.signals.get_signals_request.Field1] | None
var filters : adcp.types.generated_poc.core.signal_filters.SignalFilters | None
var if_pricing_version : str | None
var if_wholesale_feed_version : str | None
var max_results : int | None
var model_config
var pagination : adcp.types.generated_poc.core.pagination_request.PaginationRequest | None
var push_notification_config : adcp.types.generated_poc.core.push_notification_config.PushNotificationConfig | None
var signal_ids : list[adcp.types.generated_poc.core.signal_id.SignalId] | None
var signal_refs : list[adcp.types.generated_poc.core.signal_ref.SignalRef] | None
var signal_spec : str | None

Inherited members

class GetSignalsResponse (**data: Any)
Expand source code
class GetSignalsResponse(AdcpVersionEnvelope, ProtocolEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    signals: Annotated[Sequence[Signal] | None, Field(description='Array of matching signals')] = None
    errors: Annotated[
        list[error.Error] | None,
        Field(
            description='Task-specific errors and warnings (e.g., signal discovery or pricing issues)'
        ),
    ] = None
    incomplete: Annotated[
        list[IncompleteItem] | None,
        Field(
            description="Declares what the agent could not finish within the caller's time_budget or due to internal limits. Each entry identifies a scope that is missing or partial. Absent when the response is fully complete.",
            min_length=1,
        ),
    ] = None
    wholesale_feed_version: Annotated[
        str | None,
        Field(
            description="Opaque token representing the version of the wholesale signals feed state used to compose this response. Agents that implement conditional-fetch (if_wholesale_feed_version) MUST return this on every wholesale-mode response so callers can cache and probe later. Callers MUST treat the value as opaque — no format, no ordering, no inspection. The token is scope-keyed: it describes a version for the cache_scope declared on this response, NOT a global agent version. A caller caches `(cache_scope, wholesale_feed_version)` pairs and presents the matching token on the next request. Scoping dimensions: (agent, discovery_mode, filters, destinations, countries) for cache_scope: 'public'; that tuple plus account_id for cache_scope: 'account'. pagination.cursor is NOT part of the scoping tuple. See specs/wholesale-feed-webhooks.md for the full cache layering model."
        ),
    ] = None
    pricing_version: Annotated[
        str | None,
        Field(
            description='Opaque token representing the version of the pricing layer. When the agent supports independent pricing versioning, pricing_version changes when prices move but wholesale_feed_version changes only when structure/metadata moves. Same cache_scope keying as wholesale_feed_version. Agents not separating these MAY omit pricing_version and use wholesale_feed_version for both.'
        ),
    ] = None
    cache_scope: Annotated[
        CacheScope | None,
        Field(
            description="Declares whether the wholesale_feed_version and pricing_version on this response describe a universal layer or an account-specific overlay. REQUIRED on every 3.1+ response (the 3.1 schema enforces this — the safety property of the two-layer cache model depends on it). 'public': this response describes the agent's published rate card; the caller MAY dedupe under (agent, discovery_mode, filters, destinations, countries) without scoping by account. 'account': this response includes account-specific overrides; the caller MUST cache the version under that tuple plus account_id. When the request did NOT include `account`, the agent MUST return `cache_scope: 'public'`. When the request included `account`, the agent MUST return either 'public' (this account prices off the public rate card — caller dedupes) or 'account' (account-specific overrides exist — caller caches under the account key). Agents MAY return 'public' on an account-scoped request that previously had overrides — callers SHOULD interpret this as a downgrade. Without schema-required cache_scope, an agent silently omitting the field on an account-scoped response would cause callers to mis-key the cache and serve account-overlay payloads to other accounts — the canonical safety invariant of the entire cache layering model. **Backward-compatibility note for 3.1 validators:** SDKs validating strictly against the 3.1 schema MUST select the validator based on the server-declared `adcp_version`. For responses with `adcp_version` starting `3.0`, the 3.1 cache_scope-required constraint MUST be relaxed — pre-3.1 agents correctly emit no cache_scope and remain conformant to their declared version. This is a tightening within 3.1, not a 3.0 break."
        ),
    ] = CacheScope.public
    unchanged: Annotated[
        Literal[True] | None,
        Field(
            description="Present and `true` ONLY on wholesale-mode responses when the request carried if_wholesale_feed_version (and/or if_pricing_version) matching the agent's current version for the caller's cache_scope, in which case signals[] MUST be omitted; wholesale_feed_version (echoed), cache_scope (echoed), and pricing_version (echoed when used) MUST still be present. Callers receiving unchanged: true MUST NOT mutate their local wholesale signals mirror. **One shape per state:** agents MUST NOT emit `unchanged: false` — the absence of the field IS the signal that the response carries signals. **Cross-scope isolation:** the comparator that decides `unchanged` MUST be keyed on `(cache_scope, wholesale_feed_version)`, not on the token value alone. An agent MUST NOT emit `unchanged: true` when it resolves the request to a different `cache_scope` than the one whose token the caller echoed in `if_wholesale_feed_version` (and/or `if_pricing_version`): because the token is scope-keyed, a value minted for `cache_scope: 'public'` cannot match the agent's current token for `cache_scope: 'account'` (or vice-versa), so such a request MUST return the full feed for the resolved scope with that scope's own token."
        ),
    ] = None
    pagination: pagination_response.PaginationResponse | None = None
    sandbox: Annotated[
        bool | None,
        Field(description='When true, this response contains simulated data from sandbox mode.'),
    ] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • adcp.types.generated_poc.core.protocol_envelope.ProtocolEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var cache_scope : adcp.types.generated_poc.signals.get_signals_response.CacheScope | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var errors : list[adcp.types.generated_poc.core.error.Error] | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var incomplete : list[adcp.types.generated_poc.signals.get_signals_response.IncompleteItem] | None
var model_config
var pagination : adcp.types.generated_poc.core.pagination_response.PaginationResponse | None
var pricing_version : str | None
var sandbox : bool | None
var signals : collections.abc.Sequence[adcp.types.generated_poc.signals.get_signals_response.Signal] | None
var unchanged : Literal[True] | None
var wholesale_feed_version : str | None

Inherited members

class GetTaskStatusRequest (**data: Any)
Expand source code
class GetTaskStatusRequest(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    task_id: Annotated[str, Field(description='Unique identifier of the task to retrieve')]
    account: Annotated[
        account_ref.AccountReference | None,
        Field(
            description='Account scope for the task lookup. Sellers MUST return REFERENCE_NOT_FOUND for a task_id that exists only under a different account or principal. When omitted, the seller MAY use the credential-bound singleton account, but multi-account credentials SHOULD require an explicit account.'
        ),
    ] = None
    include_history: Annotated[
        bool | None,
        Field(
            description='Include full conversation history for this task (may increase response size)'
        ),
    ] = False
    include_result: Annotated[
        bool | None,
        Field(
            description="Include the task's canonical terminal result payload when one exists. Defaults to false for lightweight status-only polls. When true, sellers MUST include result for completed, failed, or rejected terminal tasks when that task produced a terminal artifact; canceled tasks may have no result. The legacy singular error field remains a convenience for failed tasks but does not replace the canonical terminal result."
        ),
    ] = False
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var account : adcp.types.generated_poc.core.account_ref.AccountReference | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var include_history : bool | None
var include_result : bool | None
var model_config
var task_id : str

Inherited members

class GetTaskStatusResponse (**data: Any)
Expand source code
class GetTaskStatusResponse(AdcpVersionEnvelope, ProtocolEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    task_id: Annotated[str, Field(description='Unique identifier for this task')]
    task_type: Annotated[task_type_1.TaskType, Field(description='Type of AdCP operation')]
    protocol: Annotated[
        adcp_protocol.AdcpProtocol, Field(description='AdCP protocol this task belongs to')
    ]
    status: Annotated[task_status.TaskStatus, Field(description='Current task status')]
    created_at: Annotated[
        AwareDatetime, Field(description='When the task was initially created (ISO 8601)')
    ]
    updated_at: Annotated[
        AwareDatetime, Field(description='When the task was last updated (ISO 8601)')
    ]
    completed_at: Annotated[
        AwareDatetime | None,
        Field(
            description='When the task completed (ISO 8601, only for completed/failed/canceled tasks)'
        ),
    ] = None
    has_webhook: Annotated[
        bool | None, Field(description='Whether this task has webhook configuration')
    ] = None
    progress: Annotated[
        Progress | None, Field(description='Progress information for long-running tasks')
    ] = None
    error: Annotated[
        Error | None,
        Field(
            description='Convenience summary for failed tasks. When include_result was true and the canonical terminal result is also present, this error MUST agree with the canonical fatal error in result. A legacy poll carrying only this singular summary proves failure status but not equivalence to a richer terminal webhook artifact.'
        ),
    ] = None
    history: Annotated[
        list[HistoryItem] | None,
        Field(
            description='Complete conversation history for this task (only included if include_history was true in request)'
        ),
    ] = None
    result: Annotated[
        dict[str, Any] | None,
        Field(
            description='Canonical task-specific terminal payload. Present when include_result was true and a completed, failed, or rejected task produced a terminal artifact; canceled tasks may omit it. For failed tasks, the singular error field is a convenience summary and MUST agree with the canonical fatal error represented here. Consumers and sellers MUST resolve and validate the exact schema through manifest.task_result_resolution: use terminal_schema_overrides[task_type] when present, otherwise tools[task_type].response_schema. The polling envelope keeps this field generic so get_task_status does not embed every task response schema.'
        ),
    ] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • adcp.types.generated_poc.core.protocol_envelope.ProtocolEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var completed_at : pydantic.types.AwareDatetime | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var created_at : pydantic.types.AwareDatetime
var error : adcp.types.generated_poc.protocol.get_task_status_response.Error | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var has_webhook : bool | None
var history : list[adcp.types.generated_poc.protocol.get_task_status_response.HistoryItem] | None
var model_config
var progress : adcp.types.generated_poc.protocol.get_task_status_response.Progress | None
var protocol : adcp.types.generated_poc.enums.adcp_protocol.AdcpProtocol
var result : dict[str, typing.Any] | None
var status : adcp.types.generated_poc.enums.task_status.TaskStatus
var task_id : str
var task_type : adcp.types.generated_poc.enums.task_type.TaskType
var updated_at : pydantic.types.AwareDatetime

Inherited members

class Gtin (root: RootModelRootType = PydanticUndefined, **data)
Expand source code
class Gtin(RootModel[str]):
    root: Annotated[str, Field(pattern='^[0-9]{8,14}$')]

Usage Documentation

RootModel and Custom Root Types

A Pydantic BaseModel for the root object of the model.

Attributes
-----=
root
The root object of the model.
__pydantic_root_model__
Whether the model is a RootModel.
__pydantic_private__
Private fields in the model.
__pydantic_extra__
Extra fields in the model.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • pydantic.root_model.RootModel[str]
  • pydantic.root_model.RootModel
  • pydantic.main.BaseModel
  • typing.Generic

Class variables

var model_config
var root : str
class HtmlContent (**data: Any)
Expand source code
class HtmlAsset(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    asset_type: Annotated[
        Literal['html'],
        Field(
            description='Discriminator identifying this as an HTML asset. See /schemas/creative/asset-types for the registry.'
        ),
    ] = 'html'
    content: Annotated[str, Field(description='HTML content')]
    version: Annotated[str | None, Field(description="HTML version (e.g., 'HTML5')")] = None
    accessibility: Annotated[
        Accessibility | None,
        Field(description='Self-declared accessibility properties for this opaque creative'),
    ] = None
    provenance: Annotated[
        provenance_1.Provenance | None,
        Field(
            description='Provenance metadata for this asset, overrides manifest-level provenance'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var accessibility : adcp.types.generated_poc.core.assets.html_asset.Accessibility | None
var asset_type : Literal['html']
var content : str
var model_config
var provenance : adcp.types.generated_poc.core.provenance.Provenance | None
var version : str | None

Inherited members

class IdempotencyConflictError (operation: str, errors: list[Any], agent_id: str | None = None)
Expand source code
class IdempotencyConflictError(ADCPTaskError):
    """Server rejected a reused idempotency_key whose payload differs from the original.

    The request used the same idempotency_key as an earlier request but with a
    materially different (post-JCS-canonicalization) payload. Two valid recovery
    paths: (a) mint a fresh ``uuid.uuid4()`` key and resubmit, or (b) resend the
    exact original payload — whichever matches the caller's intent.

    By design the rendered message does NOT include the server's error text
    because non-compliant sellers may include payload hints that violate the
    ``IDEMPOTENCY_CONFLICT`` spec requirement. Raw errors remain on
    ``self.errors`` for callers that want to inspect.
    """

    def __init__(
        self,
        operation: str,
        errors: list[Any],
        agent_id: str | None = None,
    ):
        self.operation = operation
        self.errors = errors
        self.error_codes = [e.code for e in errors if hasattr(e, "code") and e.code] or [
            "IDEMPOTENCY_CONFLICT"
        ]
        message = f"{operation}: idempotency_key reused with a different payload"
        suggestion = (
            "The server already has a response for this idempotency_key with a "
            "different (JCS-canonicalized) payload. Either resend the exact original "
            "payload, or mint a fresh key with uuid.uuid4() and resubmit. Do NOT "
            "reuse this key with modified fields."
        )
        # Skip ADCPTaskError.__init__ to avoid leaking server-supplied text.
        ADCPError.__init__(self, message, agent_id=agent_id, suggestion=suggestion)

Server rejected a reused idempotency_key whose payload differs from the original.

The request used the same idempotency_key as an earlier request but with a materially different (post-JCS-canonicalization) payload. Two valid recovery paths: (a) mint a fresh uuid.uuid4() key and resubmit, or (b) resend the exact original payload — whichever matches the caller's intent.

By design the rendered message does NOT include the server's error text because non-compliant sellers may include payload hints that violate the IDEMPOTENCY_CONFLICT spec requirement. Raw errors remain on self.errors for callers that want to inspect.

Initialize task error.

Args
-----=
operation
The task that failed (e.g., "create_media_buy")
errors
List of ADCP Error objects from the response
agent_id
Optional agent ID for context

Ancestors

Inherited members

class IdempotencyExpiredError (operation: str, errors: list[Any], agent_id: str | None = None)
Expand source code
class IdempotencyExpiredError(ADCPTaskError):
    """Server's replay cache for this idempotency_key has expired.

    Per AdCP #2315 the seller MAY discard cached responses after
    ``replay_ttl_seconds``. Re-executing is unsafe because the seller can no
    longer distinguish "seen and evicted" from "never seen" — silently retrying
    risks duplicate execution. Recovery: reconcile state via a read (e.g.
    ``get_media_buys``) before resubmitting with a fresh key.
    """

    def __init__(
        self,
        operation: str,
        errors: list[Any],
        agent_id: str | None = None,
    ):
        self.operation = operation
        self.errors = errors
        self.error_codes = [e.code for e in errors if hasattr(e, "code") and e.code] or [
            "IDEMPOTENCY_EXPIRED"
        ]
        message = f"{operation}: idempotency replay window has expired"
        suggestion = (
            "The seller's replay_ttl_seconds window for this key has passed. "
            "Re-execution is unsafe — the seller can no longer guarantee "
            "at-most-once. Reconcile state with a read (e.g. get_media_buys) "
            "before resubmitting with a fresh uuid.uuid4() key."
        )
        ADCPError.__init__(self, message, agent_id=agent_id, suggestion=suggestion)

Server's replay cache for this idempotency_key has expired.

Per AdCP #2315 the seller MAY discard cached responses after replay_ttl_seconds. Re-executing is unsafe because the seller can no longer distinguish "seen and evicted" from "never seen" — silently retrying risks duplicate execution. Recovery: reconcile state via a read (e.g. get_media_buys) before resubmitting with a fresh key.

Initialize task error.

Args
-----=
operation
The task that failed (e.g., "create_media_buy")
errors
List of ADCP Error objects from the response
agent_id
Optional agent ID for context

Ancestors

Inherited members

class IdempotencyScopeError (operation: str, agent_id: str | None = None)
Expand source code
class IdempotencyScopeError(ADCPTaskError):
    """Server cannot safely scope an idempotency_key to an authenticated caller."""

    def __init__(self, operation: str, agent_id: str | None = None):
        self.operation = operation
        self.errors = [
            {
                "code": "INVALID_REQUEST",
                "message": "idempotency_key requires authenticated caller_identity",
            }
        ]
        self.error_codes = ["INVALID_REQUEST"]
        message = f"{operation}: idempotency_key requires authenticated caller_identity"
        suggestion = (
            "Populate ToolContext.caller_identity from the authenticated principal before "
            "using idempotency replay protection. Rejecting the request avoids a shared "
            "cross-principal idempotency namespace."
        )
        ADCPError.__init__(self, message, agent_id=agent_id, suggestion=suggestion)

Server cannot safely scope an idempotency_key to an authenticated caller.

Initialize task error.

Args
-----=
operation
The task that failed (e.g., "create_media_buy")
errors
List of ADCP Error objects from the response
agent_id
Optional agent ID for context

Ancestors

Inherited members

class IdempotencyUnsupportedError (agent_id: str | None = None,
agent_uri: str | None = None,
reason: str | None = None)
Expand source code
class IdempotencyUnsupportedError(ADCPError):
    """Seller does not support idempotency replay protection on mutating requests.

    Raised before the first mutating call when ``strict_idempotency=True`` and
    either the seller's capabilities response is missing ``adcp.idempotency``,
    declares ``supported=False``, or declares ``supported=True`` without a
    ``replay_ttl_seconds`` window. Per AdCP spec, clients MUST NOT assume a
    default — a seller that does not positively declare support cannot be
    safely retried.
    """

    def __init__(
        self,
        agent_id: str | None = None,
        agent_uri: str | None = None,
        reason: str | None = None,
    ):
        detail = reason or "seller did not declare adcp.idempotency support"
        message = f"{detail}; retry safety for mutating requests cannot be guaranteed."
        suggestion = (
            "Recommended: ask the seller to declare adcp.idempotency.supported=true "
            "with a replay_ttl_seconds window in get_adcp_capabilities. To proceed "
            "without this guarantee — retries may double-charge or duplicate — "
            "construct ADCPClient with strict_idempotency=False; the caller then "
            "owns reconciliation on retry."
        )
        super().__init__(message, agent_id, agent_uri, suggestion)

Seller does not support idempotency replay protection on mutating requests.

Raised before the first mutating call when strict_idempotency=True and either the seller's capabilities response is missing adcp.idempotency, declares supported=False, or declares supported=True without a replay_ttl_seconds window. Per AdCP spec, clients MUST NOT assume a default — a seller that does not positively declare support cannot be safely retried.

Initialize exception with context.

Ancestors

  • ADCPError
  • builtins.Exception
  • builtins.BaseException

Inherited members

class IdentityMatchRequest (**data: Any)
Expand source code
class IdentityMatchRequest(AdCPBaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    field_schema: Annotated[
        AnyUrl | None,
        Field(
            alias='$schema', description='Optional schema URI for validation. Ignored at runtime.'
        ),
    ] = None
    adcp_version: Annotated[
        str | None,
        Field(
            description='Release-precision AdCP version (VERSION.RELEASE, e.g. "3.0", "3.1", "3.1-beta"). On a request: the buyer\'s release pin. Inlined here (rather than via core/version-envelope.json allOf) so this schema can keep `additionalProperties: false` — the privacy boundary on this endpoint is contract-bearing.',
            pattern='^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[a-zA-Z0-9](?:[a-zA-Z0-9.-]*[a-zA-Z0-9])?)?$',
        ),
    ] = None
    adcp_major_version: Annotated[
        int | None,
        Field(
            deprecated=True,
            description='DEPRECATED in favor of adcp_version. Removed in 4.0. Inlined alongside adcp_version to preserve strict-mode on this endpoint.',
            ge=1,
            le=99,
        ),
    ] = None
    type: Annotated[
        Literal['identity_match_request'],
        Field(description='Message type discriminator for deserialization.'),
    ] = 'identity_match_request'
    protocol_version: Annotated[
        str | None,
        Field(
            description='TMP protocol version. Allows receivers to handle semantic differences across versions.'
        ),
    ] = '1.0'
    request_id: Annotated[
        str,
        Field(
            description='Unique request identifier. MUST NOT correlate with any context match request_id.'
        ),
    ]
    seller_agent_url: Annotated[
        AnyUrl,
        Field(
            description="API endpoint URL of the seller agent issuing this request. The buyer's identity-match service uses this to resolve the active package set it has registered for this seller; when `package_ids` is omitted, evaluation occurs against that full set. If `seller_agent_url` does not match any seller for which the buyer has registered active packages, the buyer MUST return an empty `eligible_package_ids` set — it MUST NOT fall back to evaluating against another seller's active set. Compared using the AdCP URL canonicalization rules, not byte-equality — see docs/reference/url-canonicalization. Consistent with `seller_agent.agent_url` on `AvailablePackage` and `agent_url` in `adagents.json`."
        ),
    ]
    identities: Annotated[
        list[Identity],
        Field(
            description='Identity tokens for the user, each tagged with its type. Publishers SHOULD include every token they have available — the buyer resolves on whichever graph matches. Entry order is not semantically significant; buyers use their own preference order when multiple entries resolve. Duplicate `(uid_type, user_token)` pairs MUST NOT appear; routers MAY reject or dedupe. `maxItems: 3` matches the TMPX plaintext budget (~120 bytes after HPKE overhead fits three 32-byte tokens); exceeding it forces buyer-side truncation.',
            max_length=3,
            min_length=1,
        ),
    ]
    consent: Annotated[
        Consent | None,
        Field(
            description='Privacy consent signals. Buyers in regulated jurisdictions MUST NOT process the user token without consent information.'
        ),
    ] = None
    package_ids: Annotated[
        list[str] | None,
        Field(
            description="Optional. When omitted, the buyer evaluates eligibility against the full set of active packages it has registered for `seller_agent_url`. When provided, the composition of `package_ids` MUST be statistically independent of the current placement — sending only the page-specific subset would let the buyer correlate Identity Match with Context Match by comparing package sets. Two acceptable modes: (a) **all-active** — include every active package this buyer has at this publisher; (b) **fuzzed** — include a random sample of active packages, optionally padded with synthetic non-existent IDs, drawn from a distribution that does not depend on the current placement. The buyer's silent-drop behavior on unknown IDs (specified below) is what makes synthetic-ID padding safe — they do not affect the response shape and cannot leak registry membership. When both `seller_agent_url` and `package_ids` are present, the buyer evaluates against the intersection of its registered active set and `package_ids`; IDs in `package_ids` that the buyer has not registered for this seller MUST be silently ignored (not surfaced as errors) to avoid leaking registry membership back to the publisher.",
            min_length=1,
        ),
    ] = None
    country: Annotated[
        str | None,
        Field(
            description='ISO 3166-1 alpha-2 country code. Routing directive for the TMP Router — used to select the correct regional provider. The router MUST strip this field before forwarding the request to the buyer agent. Not an identity signal.',
            pattern='^[A-Z]{2}$',
        ),
    ] = None
    sealed_credentials: Annotated[
        list[SealedCredential] | None,
        Field(
            description='Optional HPKE-sealed credentials addressed to specific audiences — the network-as-RP ("issuer-as-RP"/Mechanism B) carrier. Each payload is opaque to the publisher, who relays it untouched; the inner plaintext is an `attestation` (see identities[].attestation) scoped to the audience\'s relying party. Reuses the TMPX envelope format. Router handling (normative — see docs/trusted-match/specification.mdx): the router forwards each entry only to the provider that owns its `audience_kid` (not broadcast), folds `sealed_credentials` into the per-provider re-signature canonical bytes so an injected/swapped blob breaks the signature, and includes a `sealed_credentials_hash` in the dedup cache key. Receivers decrypt only entries whose `audience_kid` they hold a key for and ignore the rest. Receivers MUST bound count and size to prevent DoS amplification.',
            max_length=8,
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var adcp_major_version : int | None
var adcp_version : str | None
var consent : adcp.types.generated_poc.trusted_match.identity_match_request.Consent | None
var country : str | None
var field_schema : pydantic.networks.AnyUrl | None
var identities : list[adcp.types.generated_poc.trusted_match.identity_match_request.Identity]
var model_config
var package_ids : list[str] | None
var protocol_version : str | None
var request_id : str
var sealed_credentials : list[adcp.types.generated_poc.trusted_match.identity_match_request.SealedCredential] | None
var seller_agent_url : pydantic.networks.AnyUrl
var type : Literal['identity_match_request']

Inherited members

class IdentityMatchResponse (**data: Any)
Expand source code
class IdentityMatchResponseRouterPublisher(AdcpVersionEnvelope, ProtocolEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    type: Annotated[
        Literal['identity_match_response'],
        Field(description='Message type discriminator for deserialization.'),
    ] = 'identity_match_response'
    request_id: Annotated[
        str, Field(description='Echoed request identifier from the identity match request')
    ]
    eligible_package_ids: Annotated[
        list[str],
        Field(
            description='Package IDs the user is eligible for. Packages not listed are ineligible.'
        ),
    ]
    serve_window_sec: Annotated[
        int,
        Field(
            description="Per-package single-shot fcap window, in seconds. After serving the user one impression on each eligible package within this window, the publisher MUST re-query Identity Match before serving from those packages again. This is NOT a router response cache TTL — it is a buyer-asserted serve throttle. Multi-impression frequency caps are handled separately by the buyer's impression tracker, which writes cap-fire events to the IdentityMatch cap-state store at the boundary regardless of this window. Maximum 300 — longer windows reduce IdentityMatch load but coarsen fcap granularity below what most campaigns require.",
            ge=1,
            le=300,
        ),
    ]
    tmpx: Annotated[
        str | None,
        Field(
            deprecated=True,
            description='DEPRECATED in favor of tmpx_providers. Routers MAY continue to populate this field for back-compat with consumers that only know the single-token shape; when both fields are present, tmpx_providers is authoritative. Single HPKE-encrypted exposure token containing the resolved user identity tokens. Wire format: kid.base64url_nopad(ciphertext) — unpadded base64url per RFC 4648 section 5 (no = characters). Publishers MUST treat this value as opaque pass-through data. Removed in 4.0.',
        ),
    ] = None
    tmpx_providers: Annotated[
        dict[Annotated[str, StringConstraints(pattern=r'^[A-Za-z0-9_]+$', min_length=1, max_length=64)], TmpxProviders] | None,
        Field(
            description="Router-populated: ordered TMPX chunk/value pairs grouped by the originating identity provider's `provider_id`. Each entry's `chunks[]` is a copy of the provider's emitted `tmpx_chunks` list, in the same order. Each chunk carries a provider-local `slot_id` (from the provider's registered `tmpx_slots`) and an opaque URL-safe `value`; the publisher's deployment configuration (see publisher-tmpx-config.json) resolves each `(provider_id, slot_id)` pair to the ad-server macro name, targeting key, VAST substitution, or play-log field for that surface. The protocol carries values and attribution only. Required by router conformance when any identity provider emitted TMPX in this request; collapsing per-provider tokens into a single string loses attribution and breaks per-provider impression accounting. Map keys MUST match the provider_id charset registered in provider-registration.json (enforced by `propertyNames`). Publishers MUST NOT parse, decode, or transform any chunk's `value` — each is an opaque URL-safe wire string substituted verbatim into the mapped destination."
        ),
    ] = None

    @model_validator(mode='after')
    def _validate_tmpx_provider_ids(self) -> IdentityMatchResponseRouterPublisher:
        if self.tmpx_providers is None:
            return self
        invalid = [
            provider_id
            for provider_id in self.tmpx_providers
            if not _PROVIDER_ID_PATTERN.fullmatch(provider_id)
        ]
        if invalid:
            raise ValueError('tmpx_providers keys must be valid provider_id values')
        return self

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • adcp.types.generated_poc.core.protocol_envelope.ProtocolEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var eligible_package_ids : list[str]
var model_config
var request_id : str
var serve_window_sec : int
var tmpx : str | None
var tmpx_providers : dict[str, adcp.types.generated_poc.trusted_match.identity_match_response.TmpxProviders] | None
var type : Literal['identity_match_response']

Inherited members

class ImageContent (**data: Any)
Expand source code
class ImageAsset(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    asset_type: Annotated[
        Literal['image'],
        Field(
            description='Discriminator identifying this as an image asset. See /schemas/creative/asset-types for the registry.'
        ),
    ] = 'image'
    url: Annotated[AnyUrl, Field(description='URL to the image asset')]
    width: Annotated[int, Field(description='Width in pixels', ge=1)]
    height: Annotated[int, Field(description='Height in pixels', ge=1)]
    file_size_bytes: Annotated[
        int | None,
        Field(
            description='Image file size in bytes. Required by agents that advertise a max_file_size_kb constraint.',
            ge=1,
        ),
    ] = None
    pixel_ratio: Annotated[
        float | None,
        Field(
            description='Intrinsic pixels per logical render pixel (for example `2` for a 600×500 image intended to render at 300×250). Optional because a validator can infer the ratio when the target format declares logical dimensions. When supplied, it MUST agree with both `width / logical_width` and `height / logical_height`; it is never a substitute for the intrinsic `width` and `height` fields.',
            gt=0.0,
        ),
    ] = None
    format: Annotated[
        str | None, Field(description='Image file format (jpg, png, gif, webp, etc.)')
    ] = None
    alt_text: Annotated[str | None, Field(description='Alternative text for accessibility')] = None
    provenance: Annotated[
        provenance_1.Provenance | None,
        Field(
            description='Provenance metadata for this asset, overrides manifest-level provenance'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var alt_text : str | None
var asset_type : Literal['image']
var file_size_bytes : int | None
var format : str | None
var height : int
var model_config
var pixel_ratio : float | None
var provenance : adcp.types.generated_poc.core.provenance.Provenance | None
var url : pydantic.networks.AnyUrl
var width : int

Inherited members

class JavascriptContent (**data: Any)
Expand source code
class JavascriptAsset(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    asset_type: Annotated[
        Literal['javascript'],
        Field(
            description='Discriminator identifying this as a JavaScript asset. See /schemas/creative/asset-types for the registry.'
        ),
    ] = 'javascript'
    content: Annotated[str, Field(description='JavaScript content')]
    module_type: Annotated[
        javascript_module_type.JavascriptModuleType | None,
        Field(description='JavaScript module type'),
    ] = None
    accessibility: Annotated[
        Accessibility | None,
        Field(description='Self-declared accessibility properties for this opaque creative'),
    ] = None
    provenance: Annotated[
        provenance_1.Provenance | None,
        Field(
            description='Provenance metadata for this asset, overrides manifest-level provenance'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var accessibility : adcp.types.generated_poc.core.assets.javascript_asset.Accessibility | None
var asset_type : Literal['javascript']
var content : str
var model_config
var module_type : adcp.types.generated_poc.enums.javascript_module_type.JavascriptModuleType | None
var provenance : adcp.types.generated_poc.core.provenance.Provenance | None

Inherited members

class KellerType (*args, **kwds)
Expand source code
class KellerType(StrEnum):
    master = 'master'
    sub_brand = 'sub_brand'
    endorsed = 'endorsed'
    independent = 'independent'

Enum where members are also (and must be) strings

Ancestors

  • enum.StrEnum
  • builtins.str
  • enum.ReprEnum
  • enum.Enum

Class variables

var endorsed
var independent
var master
var sub_brand
class LegacyFormatId (**data: Any)
Expand source code
class LegacyFormatId(FormatReferenceStructuredObject):
    """Legacy tuple that validates a URL without rewriting its wire spelling."""

    model_config = ConfigDict(extra="allow")

    # A wire-preserving string intentionally narrows the generated AnyUrl
    # field: AnyUrl appends a slash and changes the normative legacy tuple.
    agent_url: str  # type: ignore[assignment]
    id: Annotated[str, Field(pattern=r"^[a-zA-Z0-9_-]+$")]
    width: Annotated[StrictInt | None, Field(ge=1)] = None
    height: Annotated[StrictInt | None, Field(ge=1)] = None
    duration_ms: Annotated[StrictInt | StrictFloat | None, Field(ge=1)] = None

    @field_validator("agent_url")
    @classmethod
    def _validate_agent_url(cls, value: str) -> str:
        _URL_ADAPTER.validate_python(value)
        return value

    def model_dump(self, **kwargs: Any) -> dict[str, Any]:
        """Preserve the original agent_url bytes in explicit legacy output."""

        return super().model_dump(**kwargs)

Legacy tuple that validates a URL without rewriting its wire spelling.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.format_id.FormatReferenceStructuredObject
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var agent_url : str
var duration_ms : int | float | None
var height : int | None
var id : str
var model_config
var width : int | None

Methods

def model_dump(self, **kwargs: Any) ‑> dict[str, typing.Any]
Expand source code
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
    """Preserve the original agent_url bytes in explicit legacy output."""

    return super().model_dump(**kwargs)

Preserve the original agent_url bytes in explicit legacy output.

class LegacyFormatReferenceStructuredObject (**data: Any)
Expand source code
class LegacyFormatId(FormatReferenceStructuredObject):
    """Legacy tuple that validates a URL without rewriting its wire spelling."""

    model_config = ConfigDict(extra="allow")

    # A wire-preserving string intentionally narrows the generated AnyUrl
    # field: AnyUrl appends a slash and changes the normative legacy tuple.
    agent_url: str  # type: ignore[assignment]
    id: Annotated[str, Field(pattern=r"^[a-zA-Z0-9_-]+$")]
    width: Annotated[StrictInt | None, Field(ge=1)] = None
    height: Annotated[StrictInt | None, Field(ge=1)] = None
    duration_ms: Annotated[StrictInt | StrictFloat | None, Field(ge=1)] = None

    @field_validator("agent_url")
    @classmethod
    def _validate_agent_url(cls, value: str) -> str:
        _URL_ADAPTER.validate_python(value)
        return value

    def model_dump(self, **kwargs: Any) -> dict[str, Any]:
        """Preserve the original agent_url bytes in explicit legacy output."""

        return super().model_dump(**kwargs)

Legacy tuple that validates a URL without rewriting its wire spelling.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.format_id.FormatReferenceStructuredObject
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var agent_url : str
var duration_ms : int | float | None
var height : int | None
var id : str
var model_config
var width : int | None

Methods

def model_dump(self, **kwargs: Any) ‑> dict[str, typing.Any]
Expand source code
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
    """Preserve the original agent_url bytes in explicit legacy output."""

    return super().model_dump(**kwargs)

Preserve the original agent_url bytes in explicit legacy output.

Inherited members

class LegacyHmacFallback (options_for: Callable[[Mapping[str, str]], LegacyWebhookHmacOptions | None],
only_when_9421_absent: bool = True)
Expand source code
@dataclass(frozen=True)
class LegacyHmacFallback:
    """Opt-in policy for accepting HMAC-SHA256 senders during 3.x migration.

    The default behavior of the receiver is to reject any request that fails
    9421 verification. Pass an instance of this class to ``WebhookReceiverConfig``
    to accept HMAC-signed webhooks as a fallback.

    :param options_for: callback that returns a populated
        :class:`LegacyWebhookHmacOptions` given the incoming request headers.
        Your implementation resolves the sender (from Bearer, hostname, or
        legacy shared-secret tag) and returns the secret + sender_identity
        tuple the verifier needs. Return ``None`` to decline the fallback
        for this request (rejection follows the 9421-only failure path).
    :param only_when_9421_absent: when ``True`` (default), HMAC fallback only
        fires when no 9421 headers are present at all. When a request carries
        9421 headers that FAIL verification, it still rejects — preventing a
        downgrade attack where a MITM strips the 9421 signature and replaces
        it with a forged HMAC one it knows the secret for. When ``False``,
        HMAC is tried on any 9421 failure; only set this for testing or known
        homogenous sender cohorts.
    """

    options_for: Callable[[Mapping[str, str]], LegacyWebhookHmacOptions | None]
    only_when_9421_absent: bool = True

    @classmethod
    def from_shared_secret(
        cls,
        *,
        secret: bytes,
        sender_identity: str,
        only_when_9421_absent: bool = True,
        window_seconds: int = 300,
    ) -> LegacyHmacFallback:
        """Convenience constructor for the "one secret, one sender" case.

        Covers the common 3.x migration setup where the receiver has exactly
        one publisher on the legacy scheme and binds them to a known
        ``sender_identity`` (typically a buyer-defined string). For multi-
        sender or header-derived-identity setups, construct with an
        ``options_for`` callback directly.
        """
        import time as _time

        def _options_for(_headers: Mapping[str, str]) -> LegacyWebhookHmacOptions:
            return LegacyWebhookHmacOptions(
                secret=secret,
                sender_identity=sender_identity,
                now=_time.time(),
                window_seconds=window_seconds,
            )

        return cls(
            options_for=_options_for,
            only_when_9421_absent=only_when_9421_absent,
        )

Opt-in policy for accepting HMAC-SHA256 senders during 3.x migration.

The default behavior of the receiver is to reject any request that fails 9421 verification. Pass an instance of this class to WebhookReceiverConfig to accept HMAC-signed webhooks as a fallback.

:param options_for: callback that returns a populated :class:LegacyWebhookHmacOptions given the incoming request headers. Your implementation resolves the sender (from Bearer, hostname, or legacy shared-secret tag) and returns the secret + sender_identity tuple the verifier needs. Return None to decline the fallback for this request (rejection follows the 9421-only failure path). :param only_when_9421_absent: when True (default), HMAC fallback only fires when no 9421 headers are present at all. When a request carries 9421 headers that FAIL verification, it still rejects — preventing a downgrade attack where a MITM strips the 9421 signature and replaces it with a forged HMAC one it knows the secret for. When False, HMAC is tried on any 9421 failure; only set this for testing or known homogenous sender cohorts.

Static methods

def from_shared_secret(*,
secret: bytes,
sender_identity: str,
only_when_9421_absent: bool = True,
window_seconds: int = 300) ‑> LegacyHmacFallback

Convenience constructor for the "one secret, one sender" case.

Covers the common 3.x migration setup where the receiver has exactly one publisher on the legacy scheme and binds them to a known sender_identity (typically a buyer-defined string). For multi- sender or header-derived-identity setups, construct with an options_for callback directly.

Instance variables

var only_when_9421_absent : bool
var options_for : Callable[[Mapping[str, str]], LegacyWebhookHmacOptions | None]
class ListAccountsRequest (**data: Any)
Expand source code
class ListAccountsRequest(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    account: Annotated[
        account_ref.AccountReference | None,
        Field(
            description='Optional exact account filter. Use `account_id` to retrieve one known seller/storefront account, or the complete natural key (`brand` + `operator` + optional `operator_unit`, fixed `currency`, buyer-selected account `timezone`, and `sandbox`) for buyer-declared accounts. When present, the seller returns only matching accounts visible to the authenticated caller.'
        ),
    ] = None
    status: Annotated[
        Status | None,
        Field(description='Filter accounts by status. Omit to return accounts in all statuses.'),
    ] = None
    pagination: pagination_request.PaginationRequest | None = None
    sandbox: Annotated[
        bool | None,
        Field(
            description='Filter by sandbox status. true returns only sandbox accounts, false returns only production accounts. Omit to return all accounts. Primarily used with account-id namespaces where sandbox accounts are pre-existing test accounts on the platform.'
        ),
    ] = None
    include_webhook_activity: Annotated[
        bool | None,
        Field(
            description='When true, request recent webhook delivery attempts for each returned account in account.webhook_activity[]. Sellers MAY omit webhook_activity if they do not expose this debug log; when present, three-state semantics match the shared webhook_activity[] contract.'
        ),
    ] = False
    webhook_activity_limit: Annotated[
        int | None,
        Field(
            description='Maximum number of webhook_activity[] records to return per account when include_webhook_activity is true.',
            ge=1,
            le=200,
        ),
    ] = 50
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var account : adcp.types.generated_poc.core.account_ref.AccountReference | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var include_webhook_activity : bool | None
var model_config
var pagination : adcp.types.generated_poc.core.pagination_request.PaginationRequest | None
var sandbox : bool | None
var status : adcp.types.generated_poc.account.list_accounts_request.Status | None
var webhook_activity_limit : int | None

Inherited members

class ListAccountsResponse (**data: Any)
Expand source code
class ListAccountsResponse(AdcpVersionEnvelope, ProtocolEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    accounts: Annotated[
        list[account_with_authorization.AccountWithAuthorization],
        Field(
            description='Array of accounts accessible to the authenticated agent. Each entry is the full Account object plus optional authorization. Buyer-declared entries include brand, operator, and any operator_unit, fixed currency, buyer-selected account timezone, and sandbox qualifiers so the natural AccountRef round-trips after a cold start.'
        ),
    ]
    errors: Annotated[
        list[error.Error] | None, Field(description='Task-specific errors and warnings')
    ] = None
    pagination: pagination_response.PaginationResponse | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • adcp.types.generated_poc.core.protocol_envelope.ProtocolEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var accounts : list[adcp.types.generated_poc.core.account_with_authorization.AccountWithAuthorization]
var context : adcp.types.generated_poc.core.context.ContextObject | None
var errors : list[adcp.types.generated_poc.core.error.Error] | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config
var pagination : adcp.types.generated_poc.core.pagination_response.PaginationResponse | None

Inherited members

class ListContentStandardsSuccessResponse (**data: Any)
Expand source code
class ListContentStandardsResponse(AdcpVersionEnvelope, ProtocolEnvelope):
    pass

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • adcp.types.generated_poc.core.protocol_envelope.ProtocolEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var model_config
class ListContentStandardsResponse1 (**data: Any)
Expand source code
class ListContentStandardsResponse(AdcpVersionEnvelope, ProtocolEnvelope):
    pass

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • adcp.types.generated_poc.core.protocol_envelope.ProtocolEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var model_config
class ListContentStandardsErrorResponse (**data: Any)
Expand source code
class ListContentStandardsResponse(AdcpVersionEnvelope, ProtocolEnvelope):
    pass

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • adcp.types.generated_poc.core.protocol_envelope.ProtocolEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var model_config

Inherited members

class LegacyListCreativeFormatsRequest (**data: Any)
Expand source code
class ListCreativeFormatsRequest(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    format_ids: Annotated[
        list[format_id.FormatReferenceStructuredObject] | None,
        Field(
            deprecated=True,
            description='Deprecated in AdCP 3.2; removed in AdCP 4.0. Return only these specific named-format IDs (for example, from a 3.x get_products response). Use canonical-format discovery in 4.0.',
            min_length=1,
        ),
    ] = None
    asset_types: Annotated[
        list[asset_content_type.AssetContentType] | None,
        Field(
            description="Filter to formats that include these asset types. For third-party tags, search for 'html' or 'javascript'. For published-post reference formats, search for 'published_post'. E.g., ['image', 'text'] returns formats with images and text, ['javascript'] returns formats accepting JavaScript tags.",
            min_length=1,
        ),
    ] = None
    max_width: Annotated[
        int | None,
        Field(
            description='Maximum width in pixels (inclusive). Returns formats where ANY render has width <= this value. For multi-render formats, matches if at least one render fits.'
        ),
    ] = None
    max_height: Annotated[
        int | None,
        Field(
            description='Maximum height in pixels (inclusive). Returns formats where ANY render has height <= this value. For multi-render formats, matches if at least one render fits.'
        ),
    ] = None
    min_width: Annotated[
        int | None,
        Field(
            description='Minimum width in pixels (inclusive). Returns formats where ANY render has width >= this value.'
        ),
    ] = None
    min_height: Annotated[
        int | None,
        Field(
            description='Minimum height in pixels (inclusive). Returns formats where ANY render has height >= this value.'
        ),
    ] = None
    is_responsive: Annotated[
        bool | None,
        Field(
            description='Filter for responsive formats that adapt to container size. When true, returns formats without fixed dimensions.'
        ),
    ] = None
    name_search: Annotated[
        str | None, Field(description='Search for formats by name (case-insensitive partial match)')
    ] = None
    publisher_domain: Annotated[
        str | None,
        Field(
            deprecated=True,
            description="Deprecated compatibility filter for older 3.x callers. A compatibility implementation MAY project publisher-origin or community-catalog declarations obtained through the registry publisher lookup, but MUST NOT synthesize a publisher catalog from seller products. New callers use `GET /api/registry/publisher?domain=...` for publisher acceptance and `get_products` for this seller's deliverability. The pattern below is a syntactic floor, not an SSRF guard.",
            pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$',
        ),
    ] = None
    property_id: Annotated[
        property_id_1.PropertyId | None,
        Field(
            description="Filter to formats supported on the named property within the publisher's catalog. Resolves to a property in the publisher's `adagents.json` `properties[]`; the agent returns only `formats[]` entries whose `applies_to_property_ids` includes this property (or entries with no scope, which apply to all properties). Typically used in combination with `publisher_domain`."
        ),
    ] = None
    wcag_level: Annotated[
        wcag_level_1.WcagLevel | None,
        Field(
            description='Filter to formats that meet at least this WCAG conformance level (A < AA < AAA)'
        ),
    ] = None
    disclosure_positions: Annotated[
        list[disclosure_position.DisclosurePosition] | None,
        Field(
            description="Filter to formats that support all of these disclosure positions. When a format has disclosure_capabilities, match against those positions. Otherwise fall back to supported_disclosure_positions. Use to find formats compatible with a brief's compliance requirements.",
            min_length=1,
        ),
    ] = None
    disclosure_persistence: Annotated[
        list[disclosure_persistence_1.DisclosurePersistence] | None,
        Field(
            description='Filter to formats where each requested persistence mode is supported by at least one position in disclosure_capabilities. Different positions may satisfy different modes. Use to find formats compatible with jurisdiction-specific persistence requirements (e.g., continuous for EU AI Act).',
            min_length=1,
        ),
    ] = None
    output_format_ids: Annotated[
        list[format_id.FormatReferenceStructuredObject] | None,
        Field(
            description="Filter to formats whose output_format_ids includes any of these format IDs. Returns formats that can produce these outputs — inspect each result's input_format_ids to see what inputs they accept.",
            min_length=1,
        ),
    ] = None
    input_format_ids: Annotated[
        list[format_id.FormatReferenceStructuredObject] | None,
        Field(
            description="Filter to formats whose input_format_ids includes any of these format IDs. Returns formats that accept these creatives as input — inspect each result's output_format_ids to see what they can produce.",
            min_length=1,
        ),
    ] = None
    pagination: pagination_request.PaginationRequest | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var asset_types : list[adcp.types.generated_poc.enums.asset_content_type.AssetContentType] | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var disclosure_persistence : list[adcp.types.generated_poc.enums.disclosure_persistence.DisclosurePersistence] | None
var disclosure_positions : list[adcp.types.generated_poc.enums.disclosure_position.DisclosurePosition] | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var input_format_ids : list[adcp.types.generated_poc.core.format_id.FormatReferenceStructuredObject] | None
var is_responsive : bool | None
var max_height : int | None
var max_width : int | None
var min_height : int | None
var min_width : int | None
var model_config
var output_format_ids : list[adcp.types.generated_poc.core.format_id.FormatReferenceStructuredObject] | None
var pagination : adcp.types.generated_poc.core.pagination_request.PaginationRequest | None
var property_id : adcp.types.generated_poc.core.property_id.PropertyId | None
var wcag_level : adcp.types.generated_poc.enums.wcag_level.WcagLevel | None

Instance variables

var adcp_major_version : int | None
Expand source code
def __get__(self, obj: BaseModel | None, obj_type: type[BaseModel] | None = None) -> Any:
    if obj is None:
        if self.wrapped_property is not None:
            return self.wrapped_property.__get__(None, obj_type)
        raise AttributeError(self.field_name)

    warnings.warn(self.msg, DeprecationWarning, stacklevel=2)

    if self.wrapped_property is not None:
        return self.wrapped_property.__get__(obj, obj_type)
    return obj.__dict__[self.field_name]

Read-only data descriptor used to emit a runtime deprecation warning before accessing a deprecated field.

Attributes
-----=
msg
The deprecation message to be emitted.
wrapped_property
The property instance if the deprecated field is a computed field, or None.
field_name
The name of the field being deprecated.
var format_ids : list[adcp.types.generated_poc.core.format_id.FormatReferenceStructuredObject] | None
Expand source code
def __get__(self, obj: BaseModel | None, obj_type: type[BaseModel] | None = None) -> Any:
    if obj is None:
        if self.wrapped_property is not None:
            return self.wrapped_property.__get__(None, obj_type)
        raise AttributeError(self.field_name)

    warnings.warn(self.msg, DeprecationWarning, stacklevel=2)

    if self.wrapped_property is not None:
        return self.wrapped_property.__get__(obj, obj_type)
    return obj.__dict__[self.field_name]

Read-only data descriptor used to emit a runtime deprecation warning before accessing a deprecated field.

Attributes
-----=
msg
The deprecation message to be emitted.
wrapped_property
The property instance if the deprecated field is a computed field, or None.
field_name
The name of the field being deprecated.
var publisher_domain : str | None
Expand source code
def __get__(self, obj: BaseModel | None, obj_type: type[BaseModel] | None = None) -> Any:
    if obj is None:
        if self.wrapped_property is not None:
            return self.wrapped_property.__get__(None, obj_type)
        raise AttributeError(self.field_name)

    warnings.warn(self.msg, DeprecationWarning, stacklevel=2)

    if self.wrapped_property is not None:
        return self.wrapped_property.__get__(obj, obj_type)
    return obj.__dict__[self.field_name]

Read-only data descriptor used to emit a runtime deprecation warning before accessing a deprecated field.

Attributes
-----=
msg
The deprecation message to be emitted.
wrapped_property
The property instance if the deprecated field is a computed field, or None.
field_name
The name of the field being deprecated.

Inherited members

class LegacyListCreativeFormatsResponse (**data: Any)
Expand source code
class ListCreativeFormatsResponse(AdcpVersionEnvelope, ProtocolEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    formats: Annotated[
        list[format.Format],
        Field(
            deprecated=True,
            description="Deprecated named-format definitions projected for older 3.x callers. This list is neither the publisher acceptance catalog nor the seller's canonical product deliverability contract.",
        ),
    ]
    source: Annotated[
        Source | None,
        Field(
            deprecated=True,
            description='Deprecated compatibility provenance. `publisher` means publisher-origin catalog; `aao_mirror` means community catalog; `agent_derived` is retained only to parse historical 3.x responses and MUST NOT be produced by a new 3.2 implementation because seller products are not publisher authority.',
        ),
    ] = None
    creative_agents: Annotated[
        list[CreativeAgent] | None,
        Field(
            deprecated=True,
            description="Deprecated recursive discovery projection retained for historical 3.x responses. New buyers query the registry's canonical creative capability index and confirm candidates with get_adcp_capabilities; they do not recursively walk agent-provided lists.",
        ),
    ] = None
    errors: Annotated[
        list[error.Error] | None,
        Field(description='Task-specific errors and warnings (e.g., format availability issues)'),
    ] = None
    pagination: pagination_response.PaginationResponse | None = None
    sandbox: Annotated[
        bool | None,
        Field(description='When true, this response contains simulated data from sandbox mode.'),
    ] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • adcp.types.generated_poc.core.protocol_envelope.ProtocolEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var errors : list[adcp.types.generated_poc.core.error.Error] | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config
var pagination : adcp.types.generated_poc.core.pagination_response.PaginationResponse | None
var sandbox : bool | None
var status : adcp.types.generated_poc.enums.task_status.TaskStatus | None

Instance variables

var adcp_major_version : int | None
Expand source code
def __get__(self, obj: BaseModel | None, obj_type: type[BaseModel] | None = None) -> Any:
    if obj is None:
        if self.wrapped_property is not None:
            return self.wrapped_property.__get__(None, obj_type)
        raise AttributeError(self.field_name)

    warnings.warn(self.msg, DeprecationWarning, stacklevel=2)

    if self.wrapped_property is not None:
        return self.wrapped_property.__get__(obj, obj_type)
    return obj.__dict__[self.field_name]

Read-only data descriptor used to emit a runtime deprecation warning before accessing a deprecated field.

Attributes
-----=
msg
The deprecation message to be emitted.
wrapped_property
The property instance if the deprecated field is a computed field, or None.
field_name
The name of the field being deprecated.
var creative_agents : list[adcp.types.generated_poc.media_buy.list_creative_formats_response.CreativeAgent] | None
Expand source code
def __get__(self, obj: BaseModel | None, obj_type: type[BaseModel] | None = None) -> Any:
    if obj is None:
        if self.wrapped_property is not None:
            return self.wrapped_property.__get__(None, obj_type)
        raise AttributeError(self.field_name)

    warnings.warn(self.msg, DeprecationWarning, stacklevel=2)

    if self.wrapped_property is not None:
        return self.wrapped_property.__get__(obj, obj_type)
    return obj.__dict__[self.field_name]

Read-only data descriptor used to emit a runtime deprecation warning before accessing a deprecated field.

Attributes
-----=
msg
The deprecation message to be emitted.
wrapped_property
The property instance if the deprecated field is a computed field, or None.
field_name
The name of the field being deprecated.
var formats : list[adcp.types.generated_poc.core.format.Format]
Expand source code
def __get__(self, obj: BaseModel | None, obj_type: type[BaseModel] | None = None) -> Any:
    if obj is None:
        if self.wrapped_property is not None:
            return self.wrapped_property.__get__(None, obj_type)
        raise AttributeError(self.field_name)

    warnings.warn(self.msg, DeprecationWarning, stacklevel=2)

    if self.wrapped_property is not None:
        return self.wrapped_property.__get__(obj, obj_type)
    return obj.__dict__[self.field_name]

Read-only data descriptor used to emit a runtime deprecation warning before accessing a deprecated field.

Attributes
-----=
msg
The deprecation message to be emitted.
wrapped_property
The property instance if the deprecated field is a computed field, or None.
field_name
The name of the field being deprecated.
var source : adcp.types.generated_poc.media_buy.list_creative_formats_response.Source | None
Expand source code
def __get__(self, obj: BaseModel | None, obj_type: type[BaseModel] | None = None) -> Any:
    if obj is None:
        if self.wrapped_property is not None:
            return self.wrapped_property.__get__(None, obj_type)
        raise AttributeError(self.field_name)

    warnings.warn(self.msg, DeprecationWarning, stacklevel=2)

    if self.wrapped_property is not None:
        return self.wrapped_property.__get__(obj, obj_type)
    return obj.__dict__[self.field_name]

Read-only data descriptor used to emit a runtime deprecation warning before accessing a deprecated field.

Attributes
-----=
msg
The deprecation message to be emitted.
wrapped_property
The property instance if the deprecated field is a computed field, or None.
field_name
The name of the field being deprecated.

Inherited members

class ListCreativesRequest (**data: Any)
Expand source code
class ListCreativesRequest(_ListCreativesRequestBase):
    """Canonical creative read request with legacy field selection rejected."""

    @field_validator("fields")
    @classmethod
    def _reject_legacy_fields(cls, value: Any) -> Any:
        if value and any(
            is_legacy_creative_identity_key(getattr(item, "value", item)) for item in value
        ):
            raise ValueError(
                "format_id and format_ids are unavailable on the canonical list_creatives API"
            )
        return value

Canonical creative read request with legacy field selection rejected.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var model_config
class LegacyListCreativesRequest (**data: Any)
Expand source code
class ListCreativesRequest(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    filters: creative_filters.CreativeFilters | None = None
    sort: Annotated[Sort | None, Field(description='Sorting parameters')] = None
    pagination: pagination_request.PaginationRequest | None = None
    include_assignments: Annotated[
        bool | None, Field(description='Include package assignment information in response')
    ] = True
    assignment_projection: Annotated[
        AssignmentProjection | None,
        Field(
            description='Controls nested assigned_packages projection when assignments are included. all returns up to assignment_limit active assignments per creative. matching returns only assignments matching filters.indicator_types and requires that filter. Use matching for compact indicator discovery; get_media_buys remains the complete authoritative repair path when assignments_truncated is true.'
        ),
    ] = AssignmentProjection.all
    assignment_limit: Annotated[
        int | None,
        Field(
            description='Maximum assigned_packages rows returned per creative. Sellers MUST set assignments.assignments_truncated when additional qualifying rows exist.',
            ge=1,
            le=200,
        ),
    ] = 50
    include_snapshot: Annotated[
        bool | None,
        Field(
            description='Include a lightweight delivery snapshot per creative (lifetime impressions and last-served date). For detailed performance analytics, use get_creative_delivery.'
        ),
    ] = False
    include_items: Annotated[
        bool | None,
        Field(description='Include items for multi-asset formats like carousels and native ads'),
    ] = False
    include_variables: Annotated[
        bool | None,
        Field(
            description='Include dynamic content variable definitions (DCO slots) for each creative'
        ),
    ] = False
    include_pricing: Annotated[
        bool | None,
        Field(
            description='Include pricing_options on each creative. Requires account to be provided. When false or omitted, pricing is not computed.'
        ),
    ] = False
    include_purged: Annotated[
        bool | None,
        Field(
            description="Include soft-purged creative tombstones in the result set. When true, creatives destroyed via `creative.purged` with `purge_kind: soft` surface as tombstone records carrying `purged: true`, `purged_at`, and the purge reason — within the seller's webhook activity retention window (30 days from `purged_at`, MUST match `webhook-activity-record` retention). Hard-purged creatives MUST NOT appear regardless of this flag. When false or omitted, the result set excludes all purged creatives — same default as today."
        ),
    ] = False
    include_webhook_activity: Annotated[
        bool | None,
        Field(
            description='Include recent webhook activity per creative. When true, each returned creative carries a `webhook_activity[]` array of the most recent fires scoped to that creative — `creative.status_changed` and `creative.purged` deliveries. Adoption of the `webhook_activity[]` pattern per `snapshot-and-log.mdx § Webhook activity log pattern`. Retention is 30 days from `completed_at` (MUST). Three-state presence applies: omitted = seller does not surface; `[]` = persists but no recent fires; non-empty = actual records.'
        ),
    ] = False
    webhook_activity_limit: Annotated[
        int | None,
        Field(
            description="Maximum number of `webhook_activity[]` records to return per creative. Only meaningful when `include_webhook_activity: true`. Sellers MUST respect the cap; structural enforcement is provided by the response schema's `maxItems: 200` on the array.",
            ge=1,
            le=200,
        ),
    ] = 50
    account: Annotated[
        account_ref.AccountReference | None,
        Field(
            description="Account reference for pricing and access. When provided with include_pricing, the agent returns pricing_options from this account's rate card on each creative."
        ),
    ] = None
    fields: Annotated[
        list[Field1] | None,
        Field(
            description="Specific fields to include in response (omit for all fields). The 'concept' value returns both concept_id and concept_name. `format_id` is a deprecated 3.x compatibility projection; new integrations request `format_kind` and `format_option_ref`. Selecting localization automatically includes creative_id, status, assets, the selected format identity, and localization_unavailable when applicable. Selecting rights_attestation_evaluations automatically includes rights so each seller-produced result can be reconciled with the exact retained constraint and reference.",
            min_length=1,
        ),
    ] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var account : adcp.types.generated_poc.core.account_ref.AccountReference | None
var assignment_limit : int | None
var assignment_projection : adcp.types.generated_poc.creative.list_creatives_request.AssignmentProjection | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var fields : list[adcp.types.generated_poc.creative.list_creatives_request.Field1] | None
var filters : adcp.types.generated_poc.core.creative_filters.CreativeFilters | None
var include_assignments : bool | None
var include_items : bool | None
var include_pricing : bool | None
var include_purged : bool | None
var include_snapshot : bool | None
var include_variables : bool | None
var include_webhook_activity : bool | None
var model_config
var pagination : adcp.types.generated_poc.core.pagination_request.PaginationRequest | None
var sort : adcp.types.generated_poc.creative.list_creatives_request.Sort | None
var webhook_activity_limit : int | None

Instance variables

var adcp_major_version : int | None
Expand source code
def __get__(self, obj: BaseModel | None, obj_type: type[BaseModel] | None = None) -> Any:
    if obj is None:
        if self.wrapped_property is not None:
            return self.wrapped_property.__get__(None, obj_type)
        raise AttributeError(self.field_name)

    warnings.warn(self.msg, DeprecationWarning, stacklevel=2)

    if self.wrapped_property is not None:
        return self.wrapped_property.__get__(obj, obj_type)
    return obj.__dict__[self.field_name]

Read-only data descriptor used to emit a runtime deprecation warning before accessing a deprecated field.

Attributes
-----=
msg
The deprecation message to be emitted.
wrapped_property
The property instance if the deprecated field is a computed field, or None.
field_name
The name of the field being deprecated.

Inherited members

class ListCreativesResponse (**data: Any)

Base class enforcing the primary canonical runtime boundary.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var adcp_error : adcp.types.generated_poc.core.error.Error | None
var adcp_major_version : int | None
var adcp_version : str | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var context_id : str | None
var creatives : list[Creative]
var errors : list[adcp.types.generated_poc.core.error.Error] | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var format_summary : dict[str, int] | None
var governance_context : str | None
var message : str | None
var model_config
var pagination : adcp.types.generated_poc.core.pagination_response.PaginationResponse
var payload : dict[str, typing.Any] | None
var push_notification_config : adcp.types.generated_poc.core.push_notification_config.PushNotificationConfig | None
var query_summary : adcp.types.generated_poc.creative.list_creatives_response.QuerySummary
var replayed : bool | None
var sandbox : bool | None
var status : adcp.types.generated_poc.enums.task_status.TaskStatus
var status_summary : adcp.types.generated_poc.creative.list_creatives_response.StatusSummary | None
var task_id : str | None
var timestamp : pydantic.types.AwareDatetime | None
class LegacyListCreativesResponse (**data: Any)
Expand source code
class ListCreativesResponse(AdcpVersionEnvelope, ProtocolEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    query_summary: Annotated[
        QuerySummary, Field(description='Summary of the query that was executed')
    ]
    pagination: pagination_response.PaginationResponse
    creatives: Annotated[
        Sequence[Creatives | Creatives1],
        Field(description='Array of creative assets matching the query'),
    ]
    format_summary: Annotated[
        dict[Annotated[str, StringConstraints(pattern=r'^[a-zA-Z0-9_-]+$')], int] | None,
        Field(
            description='Breakdown of creatives by canonical format kind. Keys SHOULD be `format_kind` values; an implementation may append a stable option suffix when separate product or publisher options must be distinguished.'
        ),
    ] = None
    status_summary: Annotated[
        StatusSummary | None, Field(description='Breakdown of creatives by status')
    ] = None
    errors: Annotated[
        list[error.Error] | None,
        Field(description='Task-specific errors (e.g., invalid filters, account not found)'),
    ] = None
    sandbox: Annotated[
        bool | None,
        Field(description='When true, this response contains simulated data from sandbox mode.'),
    ] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • adcp.types.generated_poc.core.protocol_envelope.ProtocolEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var creatives : Sequence[adcp.types.generated_poc.creative.list_creatives_response.Creatives | adcp.types.generated_poc.creative.list_creatives_response.Creatives1]
var errors : list[adcp.types.generated_poc.core.error.Error] | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var format_summary : dict[str, int] | None
var model_config
var pagination : adcp.types.generated_poc.core.pagination_response.PaginationResponse
var query_summary : adcp.types.generated_poc.creative.list_creatives_response.QuerySummary
var sandbox : bool | None
var status : adcp.types.generated_poc.enums.task_status.TaskStatus | None
var status_summary : adcp.types.generated_poc.creative.list_creatives_response.StatusSummary | None

Instance variables

var adcp_major_version : int | None
Expand source code
def __get__(self, obj: BaseModel | None, obj_type: type[BaseModel] | None = None) -> Any:
    if obj is None:
        if self.wrapped_property is not None:
            return self.wrapped_property.__get__(None, obj_type)
        raise AttributeError(self.field_name)

    warnings.warn(self.msg, DeprecationWarning, stacklevel=2)

    if self.wrapped_property is not None:
        return self.wrapped_property.__get__(obj, obj_type)
    return obj.__dict__[self.field_name]

Read-only data descriptor used to emit a runtime deprecation warning before accessing a deprecated field.

Attributes
-----=
msg
The deprecation message to be emitted.
wrapped_property
The property instance if the deprecated field is a computed field, or None.
field_name
The name of the field being deprecated.

Inherited members

class ListProductsRequest (**data: Any)
Expand source code
class ListProductsRequest(AdCPBaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    adcp_version: version_envelope.AdcpVersion | None = None
    adcp_major_version: version_envelope.AdcpMajorVersion | None = None
    idempotency_key: Annotated[
        str | None,
        Field(
            description='Optional replay key accepted uniformly on read calls.',
            max_length=255,
            min_length=16,
            pattern='^[A-Za-z0-9_.:-]{16,255}$',
        ),
    ] = None
    context_id: Annotated[str | None, Field(min_length=1)] = None
    context: context_1.ContextObject | None = None
    governance_context: Annotated[str | None, Field(max_length=4096, min_length=1)] = None
    push_notification_config: Annotated[
        push_notification_config_1.PushNotificationConfig | None,
        Field(
            description='Uniform per-call envelope field accepted for SDK compatibility. This does not register a wholesale feed subscription; durable product.* and wholesale_feed.bulk_change subscribers are registered through sync_accounts notification_configs.'
        ),
    ] = None
    account: Annotated[
        canonical_account_ref.CanonicalAccountReference | None,
        Field(
            description='Account scope for pricing and availability. A natural-key account is the single brand source and MUST NOT be combined with top-level brand.'
        ),
    ] = None
    brand: brand_key.BrandKey | None = None
    criteria: product_discovery_criteria.ProductDiscoveryCriteria | None = None
    fields: product_fields.ProductResponseFields | None = None
    cursor: Annotated[str | None, Field(min_length=1)] = None
    max_results: Annotated[int | None, Field(ge=1, le=100)] = 25
    if_feed_version: Annotated[
        str | None,
        Field(
            description='Opaque feed version returned by a prior list_products response or wholesale product-feed webhook for the same cache scope and canonicalized selection. Used for repair and conditional reconciliation, not routine polling when webhooks are active.'
        ),
    ] = None
    if_pricing_version: Annotated[
        str | None,
        Field(
            description='Opaque pricing version returned by a prior list_products response. Valid only with if_feed_version.'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var account : adcp.types.generated_poc.core.canonical_account_ref.CanonicalAccountReference | None
var adcp_major_version : adcp.types.generated_poc.core.version_envelope.AdcpMajorVersion | None
var adcp_version : adcp.types.generated_poc.core.version_envelope.AdcpVersion | None
var brand : adcp.types.generated_poc.core.brand_key.BrandKey | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var context_id : str | None
var criteria : adcp.types.generated_poc.media_buy.product_discovery_criteria.ProductDiscoveryCriteria | None
var cursor : str | None
var fields : adcp.types.generated_poc.media_buy.product_fields.ProductResponseFields | None
var governance_context : str | None
var idempotency_key : str | None
var if_feed_version : str | None
var if_pricing_version : str | None
var max_results : int | None
var model_config
var push_notification_config : adcp.types.generated_poc.core.push_notification_config.PushNotificationConfig | None

Inherited members

class ListTasksRequest (**data: Any)
Expand source code
class ListTasksRequest(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    account: Annotated[
        account_ref.AccountReference | None,
        Field(
            description="Account scope for task reconciliation. Sellers MUST only return tasks created for the caller's authenticated account + principal pair. When omitted, the seller MAY use the credential-bound singleton account, but multi-account credentials SHOULD require an explicit account."
        ),
    ] = None
    filters: Annotated[Filters | None, Field(description='Filter criteria for querying tasks')] = (
        None
    )
    sort: Annotated[Sort | None, Field(description='Sorting parameters')] = None
    pagination: pagination_request.PaginationRequest | None = None
    include_history: Annotated[
        bool | None,
        Field(
            description='Include full conversation history for each task (may significantly increase response size)'
        ),
    ] = False
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var account : adcp.types.generated_poc.core.account_ref.AccountReference | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var filters : adcp.types.generated_poc.protocol.list_tasks_request.Filters | None
var include_history : bool | None
var model_config
var pagination : adcp.types.generated_poc.core.pagination_request.PaginationRequest | None
var sort : adcp.types.generated_poc.protocol.list_tasks_request.Sort | None

Inherited members

class ListTasksResponse (**data: Any)
Expand source code
class ListTasksResponse(AdcpVersionEnvelope, ProtocolEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    query_summary: Annotated[
        QuerySummary, Field(description='Summary of the query that was executed')
    ]
    tasks: Annotated[list[Task], Field(description='Array of tasks matching the query criteria')]
    pagination: pagination_response.PaginationResponse
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • adcp.types.generated_poc.core.protocol_envelope.ProtocolEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config
var pagination : adcp.types.generated_poc.core.pagination_response.PaginationResponse
var query_summary : adcp.types.generated_poc.protocol.list_tasks_response.QuerySummary
var tasks : list[adcp.types.generated_poc.protocol.list_tasks_response.Task]

Inherited members

class ListTransformersRequest (**data: Any)
Expand source code
class ListTransformersRequestCreativeAgent(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    transformer_ids: Annotated[
        list[str] | None,
        Field(description='Return only these specific transformer IDs.', min_length=1),
    ] = None
    input_format_ids: Annotated[
        list[format_id.FormatReferenceStructuredObject] | None,
        Field(
            deprecated=True,
            description='**DEPRECATED in 3.2.** Filter by legacy named input formats. Use input_format_kinds.',
            min_length=1,
        ),
    ] = None
    output_format_ids: Annotated[
        list[format_id.FormatReferenceStructuredObject] | None,
        Field(
            deprecated=True,
            description='**DEPRECATED in 3.2.** Filter by legacy named output formats. Use output_capability_ids.',
            min_length=1,
        ),
    ] = None
    input_format_kinds: Annotated[
        list[canonical_format_kind.CanonicalFormatKind] | None,
        Field(
            description='Filter to transformers whose canonical input_formats include any of these canonical format kinds.',
            min_length=1,
        ),
    ] = None
    output_capability_ids: Annotated[
        list[OutputCapabilityId] | None,
        Field(
            description='Filter to transformers that can produce any of these canonical creative.supported_formats capability IDs.',
            min_length=1,
        ),
    ] = None
    name_search: Annotated[
        str | None,
        Field(description='Search transformers by name (case-insensitive partial match).'),
    ] = None
    brief: Annotated[
        str | None,
        Field(
            description="Natural-language brief used to rank and filter transformers (and their enumerable option values when expanded) — e.g. 'warm female Spanish-language voiceover'. Curates to intent rather than returning the full set, the way get_products curates inventory."
        ),
    ] = None
    expand_params: Annotated[
        list[str] | None,
        Field(
            description="Param `field` names for which to return the FIRST page of account-scoped option VALUES inline on each transformer's `params[].options[]`. Omit to return param descriptors without enumerated values (the lean default). When a param's options are truncated, its `params[].options_cursor` is set — fetch the next page via `expand_pagination` (below).",
            min_length=1,
        ),
    ] = None
    expand_pagination: Annotated[
        list[ExpandPaginationItem] | None,
        Field(
            description="Fetch the NEXT page of a specific param's account-scoped options, using the `options_cursor` a prior response returned for that `(transformer, param)`. Scoped per `(transformer_id, field)` so multiple params can be paged independently. Use this instead of `expand_params` once you hold a cursor.",
            min_length=1,
        ),
    ] = None
    include_pricing: Annotated[
        bool | None,
        Field(description='Include `pricing_options` on each transformer. Requires `account`.'),
    ] = False
    account: Annotated[
        account_ref.AccountReference | None,
        Field(
            description='Account reference. Transformers are account-scoped — the returned set, the enumerable option values, and (with include_pricing) the rate card are all resolved for this credential.'
        ),
    ] = None
    pagination: pagination_request.PaginationRequest | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var account : adcp.types.generated_poc.core.account_ref.AccountReference | None
var brief : str | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var expand_pagination : list[adcp.types.generated_poc.creative.list_transformers_request.ExpandPaginationItem] | None
var expand_params : list[str] | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var include_pricing : bool | None
var input_format_ids : list[adcp.types.generated_poc.core.format_id.FormatReferenceStructuredObject] | None
var input_format_kinds : list[adcp.types.generated_poc.core.canonical_format_kind.CanonicalFormatKind] | None
var model_config
var output_capability_ids : list[adcp.types.generated_poc.creative.list_transformers_request.OutputCapabilityId] | None
var output_format_ids : list[adcp.types.generated_poc.core.format_id.FormatReferenceStructuredObject] | None
var pagination : adcp.types.generated_poc.core.pagination_request.PaginationRequest | None
var transformer_ids : list[str] | None

Inherited members

class ListTransformersResponse (**data: Any)
Expand source code
class ListTransformersResponseCreativeAgent(AdcpVersionEnvelope, ProtocolEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    transformers: Annotated[
        list[transformer.Transformer],
        Field(description='Transformer descriptors matching the query.'),
    ]
    errors: Annotated[
        list[error.Error] | None, Field(description='Task-specific errors and warnings.')
    ] = None
    pagination: pagination_response.PaginationResponse | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • adcp.types.generated_poc.core.protocol_envelope.ProtocolEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var errors : list[adcp.types.generated_poc.core.error.Error] | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config
var pagination : adcp.types.generated_poc.core.pagination_response.PaginationResponse | None
var transformers : list[adcp.types.generated_poc.core.transformer.Transformer]

Inherited members

class LogEventRequest (**data: Any)
Expand source code
class LogEventRequest(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    event_source_id: Annotated[
        str, Field(description='Event source configured on the account via sync_event_sources')
    ]
    test_event_code: Annotated[
        str | None,
        Field(
            description="Test event code for validation without affecting production data. Events with this code appear in the platform's test events UI."
        ),
    ] = None
    events: Annotated[
        list[event.Event], Field(description='Events to log', max_length=10000, min_length=1)
    ]
    idempotency_key: Annotated[
        str,
        Field(
            description='Client-generated unique key for this request. Prevents duplicate event logging on retries. MUST be unique per (seller, request) pair to prevent cross-seller correlation. Use a fresh UUID v4 for each request.',
            max_length=255,
            min_length=16,
            pattern='^[A-Za-z0-9_.:-]{16,255}$',
        ),
    ]
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var event_source_id : str
var events : list[adcp.types.generated_poc.core.event.Event]
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var idempotency_key : str
var model_config
var test_event_code : str | None

Inherited members

class LogEventSuccessResponse (**data: Any)
Expand source code
class LogEventResponse1(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    events_received: Annotated[int, Field(ge=0)]
    events_processed: Annotated[int, Field(ge=0)]
    partial_failures: list[PartialFailure] | None = None
    warnings: list[str] | None = None
    match_quality: Annotated[float, Field(ge=0, le=1)] | None = None
    sandbox: bool | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var events_processed : int
var events_received : int
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var match_quality : float | None
var model_config
var partial_failures : list[adcp.types.generated_poc.media_buy.log_event_response.PartialFailure] | None
var sandbox : bool | None
var warnings : list[str] | None
class LogEventResponse1 (**data: Any)
Expand source code
class LogEventResponse1(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    events_received: Annotated[int, Field(ge=0)]
    events_processed: Annotated[int, Field(ge=0)]
    partial_failures: list[PartialFailure] | None = None
    warnings: list[str] | None = None
    match_quality: Annotated[float, Field(ge=0, le=1)] | None = None
    sandbox: bool | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var events_processed : int
var events_received : int
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var match_quality : float | None
var model_config
var partial_failures : list[adcp.types.generated_poc.media_buy.log_event_response.PartialFailure] | None
var sandbox : bool | None
var warnings : list[str] | None

Inherited members

class LogEventErrorResponse (**data: Any)
Expand source code
class LogEventResponse2(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    errors: Annotated[list[error_1.Error], Field(min_length=1)]
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var errors : list[adcp.types.generated_poc.core.error.Error]
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config

Inherited members

class MarkdownAsset (**data: Any)
Expand source code
class MarkdownAsset(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    asset_type: Annotated[
        Literal['markdown'],
        Field(
            description='Discriminator identifying this as a markdown asset. See /schemas/creative/asset-types for the registry.'
        ),
    ] = 'markdown'
    content: Annotated[
        str,
        Field(
            description='Markdown content following CommonMark spec with optional GitHub Flavored Markdown extensions'
        ),
    ]
    language: Annotated[
        str | None,
        Field(
            description='Optional language claim for this markdown. In a materialized creative localization variant, localized-creative-asset.json requires this value to use /schemas/core/locale-tag.json and conformance requires exact equality with the enclosing variant locale. General non-localized assets retain the legacy unconstrained string for compatibility.'
        ),
    ] = None
    markdown_flavor: Annotated[
        markdown_flavor_1.MarkdownFlavor | None,
        Field(
            description='Markdown flavor used. CommonMark for strict compatibility, GFM for tables/task lists/strikethrough.'
        ),
    ] = markdown_flavor_1.MarkdownFlavor.commonmark
    allow_raw_html: Annotated[
        bool | None,
        Field(
            description='Whether raw HTML blocks are allowed in the markdown. False recommended for security.'
        ),
    ] = False

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var allow_raw_html : bool | None
var asset_type : Literal['markdown']
var content : str
var language : str | None
var markdown_flavor : adcp.types.generated_poc.enums.markdown_flavor.MarkdownFlavor | None
var model_config

Inherited members

class McpWebhookPayload (**data: Any)
Expand source code
class McpWebhookPayload(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    idempotency_key: Annotated[
        str,
        Field(
            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.',
            max_length=255,
            min_length=16,
            pattern='^[A-Za-z0-9_.:-]{16,255}$',
        ),
    ]
    notification_id: Annotated[
        str | None,
        Field(
            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_.:-]`.',
            max_length=255,
            min_length=1,
            pattern='^[A-Za-z0-9_.:-]{1,255}$',
        ),
    ] = None
    operation_id: Annotated[
        str,
        Field(
            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: Annotated[
        str,
        Field(
            description='Unique identifier for this task. Use this to correlate webhook notifications with the original task submission.'
        ),
    ]
    task_type: Annotated[
        task_type_1.TaskType,
        Field(
            description='Type of AdCP operation that triggered this webhook. Enables webhook handlers to route to appropriate processing logic.'
        ),
    ]
    protocol: Annotated[
        adcp_protocol.AdcpProtocol | None,
        Field(
            description='AdCP protocol this task belongs to. Helps classify the operation type at a high level.'
        ),
    ] = None
    status: Annotated[
        task_status.TaskStatus,
        Field(
            description='Current task status. Webhooks are triggered for status changes after initial submission.'
        ),
    ]
    timestamp: Annotated[
        AwareDatetime,
        Field(
            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: Annotated[
        str | None,
        Field(
            description='Human-readable summary of the current task state. Provides context about what happened and what action may be needed.'
        ),
    ] = None
    context_id: Annotated[
        str | None,
        Field(
            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.'
        ),
    ] = None
    token: Annotated[
        str | None,
        Field(
            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.',
            max_length=4096,
            min_length=16,
        ),
    ] = None
    result: Annotated[
        async_response_data.AdcpAsyncResponseData | None,
        Field(
            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.'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var context_id : str | None
var idempotency_key : str
var message : str | None
var model_config
var notification_id : str | None
var operation_id : str
var protocol : adcp.types.generated_poc.enums.adcp_protocol.AdcpProtocol | None
var result : adcp.types.generated_poc.core.async_response_data.AdcpAsyncResponseData | None
var status : adcp.types.generated_poc.enums.task_status.TaskStatus
var task_id : str
var task_type : adcp.types.generated_poc.enums.task_type.TaskType
var timestamp : pydantic.types.AwareDatetime
var token : str | None

Inherited members

class MediaBuy (**data: Any)
Expand source code
class MediaBuy(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    media_buy_id: Annotated[str, Field(description="Seller's unique identifier for the media buy")]
    name: Annotated[
        str | None,
        Field(
            description='Human-readable name for this media buy, shared by buyer and seller for trafficking UI display and operational communication. Sellers MUST include the persisted name on read surfaces such as get_media_buys when the media buy was created through AdCP with name. Sellers MAY omit name for media buys created outside AdCP or created without name. This display label is not an identifier or financial reference.',
            max_length=255,
            min_length=1,
            pattern='\\S',
        ),
    ] = None
    accepted_proposal_id: Annotated[
        str | None,
        Field(
            description='Current accepted commercial snapshot. Compact-lifecycle buyers pass this ID to refine_proposals after restart or handoff. Updated atomically when an amendment or negotiated cancellation is accepted.',
            max_length=255,
            min_length=1,
        ),
    ] = None
    accepted_proposal_terms_digest: Annotated[
        str | None,
        Field(
            description='Digest of the current accepted proposal commercial_terms, allowing buyers and governance agents to verify the recovered snapshot.',
            pattern='^sha256:[A-Za-z0-9_-]{43}$',
        ),
    ] = None
    account: Annotated[
        account_1.Account | None, Field(description='Account billed for this media buy')
    ] = None
    status: media_buy_status.MediaBuyStatus
    health: Annotated[
        media_buy_health.MediaBuyHealth | None,
        Field(
            description="Aggregate health based on open impairments[]. Orthogonal to status — a paused, pending, or active buy can each be impaired. Defaults to 'ok' when impairments[] is empty."
        ),
    ] = media_buy_health.MediaBuyHealth.ok
    impairments: Annotated[
        list[impairment.Impairment] | None,
        Field(
            description="Open impairments — upstream dependency state changes that affect delivery for at least one package on this buy. Empty when health is 'ok'. Sellers MUST add an entry on next sync/poll response after a referenced resource transitions to an offline state, and MUST remove the entry (flipping health to 'ok' when the array empties) when the resource returns to a serviceable state. Staleness budget: the snapshot MUST reflect the impairment within 5 minutes of impairment.observed_at regardless of buyer poll cadence — sellers cannot rely on rare buyer polls to defer write propagation. See impairment.coherence assertion for the cross-resource invariant."
        ),
    ] = None
    rejection_reason: Annotated[
        str | None,
        Field(
            description="Reason provided by the seller when status is 'rejected'. Present only when status is 'rejected'."
        ),
    ] = None
    confirmed_at: Annotated[
        AwareDatetime | None,
        Field(
            description='ISO 8601 timestamp when the seller committed to this media buy. May be null until seller commitment occurs in deferred/manual approval flows. Once populated, remains stable through later pause, resume, activation, completion, cancellation, and reporting transitions.'
        ),
    ]
    cancellation: Annotated[
        Cancellation | None,
        Field(description="Cancellation metadata. Present only when status is 'canceled'."),
    ] = None
    total_budget: Annotated[
        float, Field(description='Hard aggregate lifetime budget amount', ge=0.0)
    ]
    daily_budget_cap: Annotated[
        float | None,
        Field(
            description='Current hard aggregate spend ceiling per calendar day. Sellers MUST echo this whenever an aggregate daily cap is set. It bounds total media-buy spend without allocating or reserving spend for packages.',
            ge=0.0,
        ),
    ] = None
    budget_cap_timezone: Annotated[
        str | None,
        Field(
            description='IANA timezone defining the shared calendar-day boundary for every aggregate and package daily cap on this media buy. Sellers MUST echo it whenever any daily cap is set.'
        ),
    ] = None
    currency: Annotated[
        str | None,
        Field(
            description="Single ISO 4217 denomination for total_budget, every package budget/minimum, and every canonical BiddingPolicy monetary field. Every package's selected pricing option MUST declare this currency; packages requiring another currency belong in a separate media buy.",
            pattern='^[A-Z]{3}$',
        ),
    ] = None
    budget_allocation: Annotated[
        budget_allocation_1.BudgetAllocation | None,
        Field(
            description='Accepted cross-package budget allocation configuration. Omitted means fixed allocation for legacy buys.'
        ),
    ] = None
    pacing: Annotated[
        pacing_1.Pacing | None,
        Field(
            description='Aggregate pacing strategy for total_budget across the media-buy flight.'
        ),
    ] = None
    bidding: Annotated[
        bidding_policy.BiddingPolicy | None,
        Field(
            description='Media-buy-authored bidding policy. This is the complete default inherited by packages that omit package.bidding; `{automatic:true}` is an explicit authored automatic policy. In seller-optimized mode, cost_per/roas bind to the primary budget_allocation optimization goal. In fixed mode, inherited cost_per requires compatible package primary-goal result units and inherited roas requires value-bearing primary goals. Monetary fields use media_buy.currency; every affected pricing option MUST declare the same currency. Package overrides are permitted only where advertised; conflicts MUST be rejected atomically with BIDDING_PLACEMENT_CONFLICT.'
        ),
    ] = None
    packages: Annotated[
        list[package.Package], Field(description='Array of packages within this media buy')
    ]
    context: Annotated[
        context_1.ContextObject | None,
        Field(
            description='Opaque media-buy-level correlation data echoed unchanged from the create_media_buy request. Sellers MUST include persisted context on read surfaces such as get_media_buys when the media buy was created through AdCP with context, so buyers can reconcile seller-assigned media_buy_id values with their own tracking state. Sellers MAY omit context for media buys created outside AdCP or created without context. Sellers MUST NOT parse this object for business logic.'
        ),
    ] = None
    invoice_recipient: Annotated[
        business_entity.BusinessEntity | None,
        Field(
            description="Per-buy override for who receives the invoice. When provided, the seller invoices this entity instead of the account's default billing_entity. The seller MUST validate the invoice recipient is authorized for this account. When governance_agents are configured, the seller MUST include invoice_recipient in the check_governance request."
        ),
    ] = None
    creative_deadline: Annotated[
        AwareDatetime | None, Field(description='ISO 8601 timestamp for creative upload deadline')
    ] = None
    revision: Annotated[
        int,
        Field(
            description='Monotonically increasing optimistic concurrency token. Incremented on every mutating state change or update; reads, validation-only calls, and exact idempotency replays do not increment it. Callers SHOULD include this in update_media_buy requests intended to change state — when provided, sellers MUST reject with CONFLICT if the revision does not match the current value, and MUST enforce that comparison atomically with the write.',
            ge=1,
        ),
    ]
    created_at: Annotated[AwareDatetime | None, Field(description='Creation timestamp')] = None
    updated_at: Annotated[AwareDatetime | None, Field(description='Last update timestamp')] = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var accepted_proposal_id : str | None
var accepted_proposal_terms_digest : str | None
var account : adcp.types.generated_poc.core.account.Account | None
var bidding : adcp.types.generated_poc.core.bidding_policy.BiddingPolicy | None
var budget_allocation : adcp.types.generated_poc.core.budget_allocation.BudgetAllocation | None
var budget_cap_timezone : str | None
var cancellation : adcp.types.generated_poc.core.media_buy.Cancellation | None
var confirmed_at : pydantic.types.AwareDatetime | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var created_at : pydantic.types.AwareDatetime | None
var creative_deadline : pydantic.types.AwareDatetime | None
var currency : str | None
var daily_budget_cap : float | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var health : adcp.types.generated_poc.enums.media_buy_health.MediaBuyHealth | None
var impairments : list[adcp.types.generated_poc.core.impairment.Impairment] | None
var invoice_recipient : adcp.types.generated_poc.core.business_entity.BusinessEntity | None
var media_buy_id : str
var model_config
var name : str | None
var pacing : adcp.types.generated_poc.enums.pacing.Pacing | None
var packages : list[adcp.types.generated_poc.core.package.Package]
var rejection_reason : str | None
var revision : int
var status : adcp.types.generated_poc.enums.media_buy_status.MediaBuyStatus
var total_budget : float
var updated_at : pydantic.types.AwareDatetime | None

Inherited members

class MediaBuyDeliveryWebhookResult (**data: Any)
Expand source code
class MediaBuyDeliveryWebhookResult(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    notification_type: Annotated[
        NotificationType,
        Field(
            description='Type of delivery-report notification: scheduled = regular periodic update, final = campaign completed, delayed = data not yet available, adjusted = corrected data for the same window, window_update = a wider measurement window supersedes a prior window.'
        ),
    ]
    partial_data: Annotated[
        bool | None,
        Field(
            description='Indicates if any media buys in this webhook have missing or delayed data.'
        ),
    ] = None
    unavailable_count: Annotated[
        int | None,
        Field(
            description='Number of media buys with reporting_delayed or failed status when partial_data is true.',
            ge=0,
        ),
    ] = None
    sequence_number: Annotated[
        int | None,
        Field(
            description='Sequential notification number for this reporting webhook stream.', ge=1
        ),
    ] = None
    next_expected_at: Annotated[
        AwareDatetime | None,
        Field(
            description='ISO 8601 timestamp for the next expected notification. Omitted on final notifications.'
        ),
    ] = None
    reporting_period: Annotated[
        ReportingPeriod, Field(description='UTC date range covered by the delivery report.')
    ]
    currency: Annotated[str, Field(description='ISO 4217 currency code.', pattern='^[A-Z]{3}$')]
    attribution_window: Annotated[
        attribution_window_1.AttributionWindow | None,
        Field(
            description='Attribution methodology and lookback windows used for conversion metrics in this report.'
        ),
    ] = None
    media_buy_deliveries: Annotated[
        list[MediaBuyDelivery],
        Field(
            description='Delivery rows for one or more media buys included in this notification.'
        ),
    ]
    errors: Annotated[
        list[error.Error] | None, Field(description='Task-specific delivery errors or warnings.')
    ] = None
    sandbox: bool | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var attribution_window : adcp.types.generated_poc.core.attribution_window.AttributionWindow | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var currency : str
var errors : list[adcp.types.generated_poc.core.error.Error] | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var media_buy_deliveries : list[adcp.types.generated_poc.media_buy.media_buy_delivery_webhook_result.MediaBuyDelivery]
var model_config
var next_expected_at : pydantic.types.AwareDatetime | None
var notification_type : adcp.types.generated_poc.media_buy.media_buy_delivery_webhook_result.NotificationType
var partial_data : bool | None
var reporting_period : adcp.types.generated_poc.media_buy.media_buy_delivery_webhook_result.ReportingPeriod
var sandbox : bool | None
var sequence_number : int | None
var unavailable_count : int | None

Inherited members

class MediaBuyStatus (*args, **kwds)
Expand source code
class MediaBuyStatus(StrEnum):
    pending_creatives = 'pending_creatives'
    pending_start = 'pending_start'
    active = 'active'
    paused = 'paused'
    completed = 'completed'
    rejected = 'rejected'
    canceled = 'canceled'

Enum where members are also (and must be) strings

Ancestors

  • enum.StrEnum
  • builtins.str
  • enum.ReprEnum
  • enum.Enum

Class variables

var active
var canceled
var completed
var paused
var pending_creatives
var pending_start
var rejected
class MediaChannel (*args, **kwds)
Expand source code
class MediaChannel(StrEnum):
    display = 'display'
    olv = 'olv'
    social = 'social'
    search = 'search'
    ctv = 'ctv'
    linear_tv = 'linear_tv'
    radio = 'radio'
    streaming_audio = 'streaming_audio'
    podcast = 'podcast'
    dooh = 'dooh'
    ooh = 'ooh'
    print = 'print'
    cinema = 'cinema'
    email = 'email'
    gaming = 'gaming'
    retail_media = 'retail_media'
    influencer = 'influencer'
    affiliate = 'affiliate'
    product_placement = 'product_placement'
    sponsored_intelligence = 'sponsored_intelligence'

Enum where members are also (and must be) strings

Ancestors

  • enum.StrEnum
  • builtins.str
  • enum.ReprEnum
  • enum.Enum

Class variables

var affiliate
var cinema
var ctv
var display
var dooh
var email
var gaming
var influencer
var linear_tv
var olv
var ooh
var podcast
var print
var product_placement
var radio
var retail_media
var search
var social
var sponsored_intelligence
var streaming_audio
class Member (**data: Any)
Expand source code
class Member(BaseModel):
    """An organization registered in the AAO member directory."""

    model_config = ConfigDict(extra="allow")

    id: str
    slug: str
    display_name: str
    description: str | None = None
    tagline: str | None = None
    logo_url: str | None = None
    logo_light_url: str | None = None
    logo_dark_url: str | None = None
    contact_email: str | None = None
    contact_website: str | None = None
    offerings: list[str] = Field(default_factory=list)
    markets: list[str] = Field(default_factory=list)
    agents: list[dict[str, Any]] = Field(default_factory=list)
    brands: list[dict[str, Any]] = Field(default_factory=list)
    is_public: bool = True
    is_founding_member: bool = False
    featured: bool = False
    si_enabled: bool = False

An organization registered in the AAO member directory.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • pydantic.main.BaseModel

Class variables

var agents : list[dict[str, typing.Any]]
var brands : list[dict[str, typing.Any]]
var contact_email : str | None
var contact_website : str | None
var description : str | None
var display_name : str
var featured : bool
var id : str
var is_founding_member : bool
var is_public : bool
var logo_dark_url : str | None
var logo_light_url : str | None
var logo_url : str | None
var markets : list[str]
var model_config
var offerings : list[str]
var si_enabled : bool
var slug : str
var tagline : str | None
class MemoryBackend (*, clock: Callable[[], float] = <built-in function time>)
Expand source code
class MemoryBackend(IdempotencyBackend):
    """In-process dict-backed store.

    Suitable for tests, single-process reference implementations, and local
    development. **Not suitable for multi-process deployments** — each worker
    has its own cache, so a retry that lands on a different worker is treated
    as a fresh request.

    Thread safety: the backend uses an :class:`asyncio.Lock` to serialize
    mutations of the shared dict. Reads go through the lock too; for a pure
    in-process backend this is cheap and prevents torn reads across concurrent
    ``get``/``put`` interleaving.

    :param clock: Callable returning the current epoch seconds. Override for
        tests that need to advance time deterministically without monkeypatching
        :mod:`time`. Defaults to :func:`time.time`.
    """

    def __init__(self, *, clock: Callable[[], float] = time.time) -> None:
        self._store: dict[tuple[str, str], CachedResponse] = {}
        self._lock = asyncio.Lock()
        self._key_locks: weakref.WeakValueDictionary[tuple[str, str], asyncio.Lock] = (
            weakref.WeakValueDictionary()
        )
        self._clock = clock

    async def get(self, scope_key: str, key: str) -> CachedResponse | None:
        async with self._lock:
            entry = self._store.get((scope_key, key))
            if entry is None:
                return None
            if entry.expires_at_epoch <= self._clock():
                # Lazy expiry — drop the stale entry so the next request
                # treats the slot as fresh and races to repopulate.
                del self._store[(scope_key, key)]
                return None
            return entry

    async def put(
        self,
        scope_key: str,
        key: str,
        entry: CachedResponse,
    ) -> None:
        async with self._lock:
            self._store[(scope_key, key)] = entry

    async def current_time(self) -> float:
        return self._clock()

    @asynccontextmanager
    async def hold(self, scope_key: str, key: str) -> AsyncIterator[None]:
        """Serialize one idempotent handler execution in this process."""
        slot = (scope_key, key)
        async with self._lock:
            key_lock = self._key_locks.get(slot)
            if key_lock is None:
                key_lock = asyncio.Lock()
                self._key_locks[slot] = key_lock
        async with key_lock:
            yield

    async def put_if_absent(self, scope_key: str, key: str, entry: CachedResponse) -> bool:
        """Atomically claim a missing or expired slot."""
        slot = (scope_key, key)
        async with self._lock:
            existing = self._store.get(slot)
            if existing is not None and existing.expires_at_epoch > self._clock():
                return False
            self._store[slot] = entry
            return True

    async def delete_expired(self, now_epoch: float | None = None) -> int:
        cutoff = now_epoch if now_epoch is not None else self._clock()
        async with self._lock:
            stale = [k for k, v in self._store.items() if v.expires_at_epoch <= cutoff]
            for k in stale:
                del self._store[k]
            return len(stale)

    async def clear(self) -> None:
        """Remove all cached entries.

        Test-suite hook — handy for resetting state between fixtures when a
        single :class:`MemoryBackend` is shared across multiple tests.
        """
        async with self._lock:
            self._store.clear()

    async def _size(self) -> int:
        """Test-only: return the current entry count."""
        async with self._lock:
            return len(self._store)

In-process dict-backed store.

Suitable for tests, single-process reference implementations, and local development. Not suitable for multi-process deployments — each worker has its own cache, so a retry that lands on a different worker is treated as a fresh request.

Thread safety: the backend uses an :class:asyncio.Lock to serialize mutations of the shared dict. Reads go through the lock too; for a pure in-process backend this is cheap and prevents torn reads across concurrent get/put interleaving.

:param clock: Callable returning the current epoch seconds. Override for tests that need to advance time deterministically without monkeypatching :mod:time. Defaults to :func:time.time.

Ancestors

Methods

async def clear(self) ‑> None
Expand source code
async def clear(self) -> None:
    """Remove all cached entries.

    Test-suite hook — handy for resetting state between fixtures when a
    single :class:`MemoryBackend` is shared across multiple tests.
    """
    async with self._lock:
        self._store.clear()

Remove all cached entries.

Test-suite hook — handy for resetting state between fixtures when a single :class:MemoryBackend is shared across multiple tests.

async def hold(self, scope_key: str, key: str) ‑> AsyncIterator[None]
Expand source code
@asynccontextmanager
async def hold(self, scope_key: str, key: str) -> AsyncIterator[None]:
    """Serialize one idempotent handler execution in this process."""
    slot = (scope_key, key)
    async with self._lock:
        key_lock = self._key_locks.get(slot)
        if key_lock is None:
            key_lock = asyncio.Lock()
            self._key_locks[slot] = key_lock
    async with key_lock:
        yield

Serialize one idempotent handler execution in this process.

async def put_if_absent(self, scope_key: str, key: str, entry: CachedResponse) ‑> bool
Expand source code
async def put_if_absent(self, scope_key: str, key: str, entry: CachedResponse) -> bool:
    """Atomically claim a missing or expired slot."""
    slot = (scope_key, key)
    async with self._lock:
        existing = self._store.get(slot)
        if existing is not None and existing.expires_at_epoch > self._clock():
            return False
        self._store[slot] = entry
        return True

Atomically claim a missing or expired slot.

Inherited members

class NativeMacroMapping (native: str)
Expand source code
@dataclass(frozen=True, slots=True)
class NativeMacroMapping:
    """A downstream ad-server token inserted without percent-encoding."""

    native: str

A downstream ad-server token inserted without percent-encoding.

Instance variables

var native : str
Expand source code
@dataclass(frozen=True, slots=True)
class NativeMacroMapping:
    """A downstream ad-server token inserted without percent-encoding."""

    native: str
class NotificationConfig (**data: Any)
Expand source code
class NotificationConfig(AdCPBaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    subscriber_id: Annotated[
        str,
        Field(
            description="Buyer-supplied identifier for this subscription endpoint. This is the stable logical key within one account's notification_configs[] set: re-sending the same subscriber_id for the same account replaces that subscriber's URL, event_types, authentication selector, and active flag rather than creating a duplicate. Echoed on every webhook payload and on every `webhook_activity[]` record fired against this config so the buyer can attribute fires across multiple endpoints. MUST be unique within the account's `notification_configs[]`. Sending two entries with the same `subscriber_id` in a single `sync_accounts` request array is rejected as a per-account validation failure with `INVALID_REQUEST` or `VALIDATION_ERROR`, and `error.field` MUST point at the duplicate entry. `subscriber_id` is the stable match key for the per-account declarative-replace diff. Always required (even with a single subscriber) so the SDK contract is uniform — no conditional required-when-multiple rules to trip up implementations. Format is opaque — recommended values are short kebab-case slugs (`buyer-primary`, `audit-bus`, `dx-team`).",
            max_length=64,
            min_length=1,
            pattern='^[A-Za-z0-9_.:-]{1,64}$',
        ),
    ]
    url: Annotated[
        AnyUrl,
        Field(
            description='Webhook endpoint URL. Same wire contract as `push-notification-config.url` — `format: "uri"`, no destination-port allowlist enforced by the protocol, SSRF protection via the IP-range check defined in docs/building/by-layer/L1/security.mdx#webhook-url-validation-ssrf. Sellers MUST validate URL syntax, HTTPS usage, hostname normalization, and reserved-range rejection when writing any config, including `active: false` configs. Sellers MUST complete an activation challenge or equivalent proof-of-control before treating a new or changed active subscriber as active.'
        ),
    ]
    event_types: Annotated[
        list[EventType],
        Field(
            description='Account-anchored notification types this subscriber wishes to receive on the registered `url`. The seller MUST NOT fire other types against this endpoint, and MUST NOT silently widen the filter when new account-anchored types are added. Creative lifecycle, assignment, indicator, account status, and wholesale feed events are valid here; media-buy-anchored types (`scheduled`, `final`, `delayed`, `adjusted`, `window_update`, `impairment`) and agent-anchored types (`capabilities.changed`) are schema-invalid on this surface and sellers MUST reject those entries as per-account validation failures with `INVALID_REQUEST` or `VALIDATION_ERROR` and `error.field` pointing at the invalid `event_types` entry rather than silently dropping them.',
            min_length=1,
        ),
    ]
    product_payload_view: Annotated[
        ProductPayloadView | None,
        Field(
            description='Product webhook representation selected by this subscriber. Use canonical with lifecycle_tools.list_products; legacy is the default for 3.x get_products consumers. Sellers emit exactly canonical_product/canonical_pricing_options or product/pricing_options accordingly. Valid only when event_types includes a product.* event.'
        ),
    ] = ProductPayloadView.legacy
    authentication: Annotated[
        Authentication | None,
        Field(
            deprecated=True,
            description="Legacy authentication selector. Same precedence and semantics as `push-notification-config.authentication` — presence opts the seller into Bearer or HMAC-SHA256 signing; absence selects the default RFC 9421 webhook profile keyed off the seller's brand.json `agents[]` JWKS. The same signed-registration downgrade-resistance rules apply to accounts[].notification_configs[].authentication. Deprecated; removed in AdCP 4.0. Credentials are write-only and MUST NOT be echoed on `list_accounts` reads.",
        ),
    ] = None
    active: Annotated[
        bool | None,
        Field(
            description="When false, the seller persists the configuration but suppresses fires. Use to pause a noisy subscriber without losing the registration. Sellers MUST NOT skip persisting the entry when `active: false` — the buyer's next `sync_accounts` MUST observe the same array, otherwise the buyer cannot distinguish pause from drop. Paused configs may skip only the outbound proof challenge while inactive; sellers MUST still enforce URL parsing, HTTPS, hostname normalization, and reserved-range rejection at write time. Reactivation requires full SSRF validation with connect pinning plus proof-of-control for any tuple without current valid proof."
        ),
    ] = True
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Subclasses

  • adcp.types.projections._NotificationConfigResponse

Class variables

var active : bool | None
var authentication : adcp.types.generated_poc.core.notification_config.Authentication | None
var event_types : list[adcp.types.generated_poc.core.notification_config.EventType]
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config
var product_payload_view : adcp.types.generated_poc.core.notification_config.ProductPayloadView | None
var subscriber_id : str
var url : pydantic.networks.AnyUrl

Inherited members

class OfferingAssetConstraint (**data: Any)
Expand source code
class OfferingAssetConstraint(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    asset_group_id: Annotated[
        str,
        Field(
            description="The asset group this constraint applies to. Values are canonical-format vocabulary — each declaration chooses its own group IDs (e.g., 'headlines', 'images', 'videos'). Buyers discover them through Product.format_options[], publisher adagents.json formats[], or creative.supported_formats[] according to context."
        ),
    ]
    asset_type: Annotated[
        asset_content_type.AssetContentType,
        Field(description='The expected content type for this group.'),
    ]
    required: Annotated[
        bool | None,
        Field(
            description='Whether this asset group must be present in each offering. Defaults to true.'
        ),
    ] = True
    min_count: Annotated[
        int | None, Field(description='Minimum number of items required in this group.', ge=1)
    ] = None
    max_count: Annotated[
        int | None, Field(description='Maximum number of items allowed in this group.', ge=1)
    ] = None
    asset_requirements: Annotated[
        asset_requirements_1.AssetRequirements | None,
        Field(
            description='Technical requirements for each item in this group (e.g., max_length for text, min_width/aspect_ratio for images). Applies uniformly to all items in the group.'
        ),
    ] = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var asset_group_id : str
var asset_requirements : adcp.types.generated_poc.core.requirements.asset_requirements.AssetRequirements | None
var asset_type : adcp.types.generated_poc.enums.asset_content_type.AssetContentType
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var max_count : int | None
var min_count : int | None
var model_config
var required : bool | None

Inherited members

class OfferingAssetGroup (**data: Any)
Expand source code
class OfferingAssetGroup(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    asset_group_id: Annotated[
        str,
        Field(
            description="Identifies the creative role this group fills. Values are defined by each canonical format declaration's offering_asset_constraints — not protocol constants. Discover creative-agent declarations via get_adcp_capabilities creative.supported_formats[] and sales-product declarations via Product.format_options[] (e.g., 'headlines', 'images', or 'videos')."
        ),
    ]
    asset_type: Annotated[
        asset_content_type.AssetContentType,
        Field(description='The content type of all items in this group.'),
    ]
    items: Annotated[
        list[Items],
        Field(
            description='The assets in this group. Each item carries an `asset_type` discriminator that selects the matching asset schema. Note: the group-level `asset_type` declares the expected type; individual items must also self-tag so validators can narrow errors. Intentionally excludes `brief-asset` and `catalog-asset` — those are campaign-input metadata types, not delivery-ready creative assets suitable for a pooled offering group. See core/assets/asset-union.json for the full asset-variant union.',
            min_length=1,
        ),
    ]
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var asset_group_id : str
var asset_type : adcp.types.generated_poc.enums.asset_content_type.AssetContentType
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var items : list[adcp.types.generated_poc.core.offering_asset_group.Items]
var model_config

Inherited members

class OptimizationGoal (root: RootModelRootType = PydanticUndefined, **data)
Expand source code
class OptimizationGoal(RootModel[OptimizationGoal8 | OptimizationGoal9 | OptimizationGoal10]):
    root: Annotated[
        OptimizationGoal8 | OptimizationGoal9 | OptimizationGoal10,
        Field(
            description='A single objective function: what to maximize or optimize, in what units, and in what priority order. Used on packages to optimize delivery within one package and on seller-optimized budget allocations to allocate spend across packages. Currency-bearing execution policy belongs in BiddingPolicy in 3.2. Legacy target.cost_per and target.per_ad_spend remain accepted only on package goals for migration and are deprecated. The primary goal is the earliest array entry among goals with the lowest explicit numeric priority; goals without priority follow all explicitly prioritized goals; when all priorities are omitted, the first entry is primary. This array-order tie-break makes duplicate priorities deterministic.',
            discriminator='kind',
            title='Optimization Goal',
        ),
    ]
    def __getattr__(self, name: str) -> Any:
        """Proxy attribute access to the wrapped type."""
        if name.startswith('_'):
            raise AttributeError(name)
        return getattr(self.root, name)

Usage Documentation

RootModel and Custom Root Types

A Pydantic BaseModel for the root object of the model.

Attributes
-----=
root
The root object of the model.
__pydantic_root_model__
Whether the model is a RootModel.
__pydantic_private__
Private fields in the model.
__pydantic_extra__
Extra fields in the model.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • pydantic.root_model.RootModel[Union[OptimizationGoal8, OptimizationGoal9, OptimizationGoal10]]
  • pydantic.root_model.RootModel
  • pydantic.main.BaseModel
  • typing.Generic

Class variables

var model_config
var root : adcp.types.generated_poc.core.optimization_goal.OptimizationGoal8 | adcp.types.generated_poc.core.optimization_goal.OptimizationGoal9 | adcp.types.generated_poc.core.optimization_goal.OptimizationGoal10
class Overlay (**data: Any)
Expand source code
class Overlay(AdCPBaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    id: Annotated[
        str,
        Field(
            description="Identifier for this overlay (e.g., 'play_pause', 'volume', 'publisher_logo', 'carousel_prev', 'carousel_next')"
        ),
    ]
    description: Annotated[
        str | None,
        Field(
            description='Human-readable explanation of what this overlay is and how buyers should account for it'
        ),
    ] = None
    visual: Annotated[
        Visual | None,
        Field(
            description='Optional visual reference for this overlay element. Useful for creative agents compositing previews and for buyers understanding what will appear over their content. Must include at least one of: url, light, or dark.'
        ),
    ] = None
    bounds: Annotated[
        Bounds,
        Field(
            description="Position and size of the overlay relative to the asset's own top-left corner. See 'unit' for coordinate interpretation."
        ),
    ]

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var bounds : adcp.types.generated_poc.core.overlay.Bounds
var description : str | None
var id : str
var model_config
var visual : adcp.types.generated_poc.core.overlay.Visual | None

Inherited members

class LegacyPackage (**data: Any)
Expand source code
class Package(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    package_id: Annotated[str, Field(description="Seller's unique identifier for the package")]
    product_id: Annotated[
        str | None,
        Field(
            description="ID of the product this package is based on. For packages created from an explicit create_media_buy package request, sellers MUST echo the request package's product_id on every response package object that represents that requested package."
        ),
    ] = None
    audience_evidence_selections: Annotated[
        list[audience_evidence_selection.AudienceEvidenceSelection] | None,
        Field(
            description='Exact immutable audience-evidence snapshots that affected recommendation, eligibility, or package construction. A confirmed package MUST include a package_construction selection matching every buyer audience_evidence_pin and every snapshot used to satisfy package audience_evidence_requirements; this readback remains mandatory on subsequent package read surfaces. This is decision provenance only; applied targeting remains exclusively in targeting_overlay and targeting_resolution.demographics.',
            min_length=1,
        ),
    ] = None
    budget: Annotated[
        float | None,
        Field(
            description='Hard lifetime spend cap for this package in the media-buy currency. Every selected pricing option in an AdCP-authored media buy MUST declare that same currency. In seller-optimized allocation mode this is a ceiling, not a current allocation. May be omitted when the package is bounded only by the shared media-buy total.',
            ge=0.0,
        ),
    ] = None
    min_spend_target: Annotated[
        float | None,
        Field(
            description='Soft lifetime spend target accepted for this package under seller-optimized budget allocation. This is an allocation preference, not a billing or delivery guarantee.',
            ge=0.0,
        ),
    ] = None
    daily_budget_cap: Annotated[
        float | None,
        Field(
            description="The hard package spend ceiling per shared media-buy cap day, in the media buy's currency. Sellers MUST echo this whenever a package daily cap is set. It is a subordinate ceiling, not a reserved or current allocation; the media buy's budget_cap_timezone defines its day boundary.",
            ge=0.0,
        ),
    ] = None
    pacing: pacing_1.Pacing | None = None
    pricing_option_id: Annotated[
        str | None,
        Field(
            description="ID of the selected pricing option from the product's pricing_options array"
        ),
    ] = None
    bid_price: Annotated[
        float | None,
        Field(
            deprecated=True,
            description='DEPRECATED legacy bidding representation. 3.2 sellers normalize accepted legacy input and SHOULD echo bidding instead. Removed in the next major.',
            ge=0.0,
        ),
    ] = None
    bidding: Annotated[
        bidding_policy.BiddingPolicy | None,
        Field(
            description='Package-authored bidding policy, echoed only when the buyer authored a package override. `{automatic:true}` is an explicit automatic-bidding override. Omission means the package inherits media-buy bidding or, when both scopes are absent, uses provider automatic delivery. Monetary fields are denominated in the media-buy currency. Sellers MUST NOT materialize inherited media-buy policy here.'
        ),
    ] = None
    price_breakdown: Annotated[
        price_breakdown_1.PriceBreakdown | None,
        Field(
            description="Breakdown of the effective price for this package. On fixed-price packages, echoes the pricing option's breakdown. On auction packages, shows the clearing price breakdown including any commission or settlement terms."
        ),
    ] = None
    impressions: Annotated[
        float | None, Field(description='Impression goal for this package', ge=0.0)
    ] = None
    catalogs: Annotated[
        list[catalog.Catalog] | None,
        Field(
            description='Catalogs this package promotes. Each catalog MUST have a distinct type (e.g., one product catalog, one store catalog). This constraint is enforced at the application level — sellers MUST reject requests containing multiple catalogs of the same type with a validation_error. Echoed from the create_media_buy request.'
        ),
    ] = None
    format_ids: Annotated[
        list[format_id.FormatReferenceStructuredObject] | None,
        Field(
            deprecated=True,
            description='Deprecated in AdCP 3.2; removed in AdCP 4.0. Legacy named-format IDs supplied for this package on create_media_buy. Sellers SHOULD echo this field whenever the request included it, including dual-emission cases where `format_option_refs` was the winning selector, so read surfaces preserve the original wire contract. Omitted means the request did not carry legacy format_ids unless the seller cannot reconstruct legacy requests created before this field was persisted.',
        ),
    ] = None
    format_option_refs: Annotated[
        list[format_option_ref.FormatOptionReference] | None,
        Field(
            description='Structured 3.1+ format option references supplied for this package on create_media_buy. Sellers SHOULD echo this field whenever the request included it. Publisher-catalog-backed options are identified by `{ scope: "publisher", publisher_domain, format_option_id }`; product-local options are identified by `{ scope: "product", format_option_id }` and resolve only against this package\'s target product. Omitted means the request did not carry format_option_refs unless the seller cannot reconstruct legacy requests created before this field was persisted.',
            min_length=1,
        ),
    ] = None
    format_kind: Annotated[
        canonical_format_kind.CanonicalFormatKind | None,
        Field(
            description='Direct canonical selector supplied for this package on create_media_buy. Sellers SHOULD echo this field whenever the request included it, including informational-echo cases where `format_ids` was the winning selector, so read surfaces preserve the original wire contract.'
        ),
    ] = None
    params: Annotated[
        dict[str, Any] | None,
        Field(
            description='Parameters for the direct canonical selector in `format_kind`, echoed from the create_media_buy request whenever the request included it. Requires `format_kind`; omitted only when the request did not carry direct canonical params or when the seller cannot reconstruct legacy requests created before this field was persisted.'
        ),
    ] = None
    targeting_overlay: Annotated[
        targeting.TargetingOverlay | None,
        Field(
            description='Complete effective targeting accepted for this package, including targeting bound through configured product selection plus package-specific targeting. Sellers MUST echo placement, property, and collection selection so buyers can audit purchased inventory.'
        ),
    ] = None
    targeting_resolution: Annotated[
        package_targeting_resolution.PackageTargetingResolution | None,
        Field(
            description="Execution details for the package's accepted targeting. Sellers MUST include targeting_resolution.demographics whenever demographic targeting was requested or applied."
        ),
    ] = None
    measurement_terms: Annotated[
        measurement_terms_1.MeasurementTerms | None,
        Field(
            description="Agreed billing measurement and makegood terms for this package. Reflects what was negotiated — may differ from the buyer's proposal or the product's defaults. When present, these terms are binding for the package's duration."
        ),
    ] = None
    performance_standards: Annotated[
        list[performance_standard.PerformanceStandard] | None,
        Field(
            description='Agreed performance standards for this package. When any entry specifies a vendor, creatives assigned to this package MUST include corresponding tracker_script or tracker_pixel assets from that vendor.',
            min_length=1,
        ),
    ] = None
    committed_metrics: Annotated[
        list[committed_metric.CommittedMetric] | None,
        Field(
            description="The binding reporting contract for this package — what the seller has agreed to populate in delivery reports. Each entry carries an explicit `committed_at` timestamp, so the array also serves as the contract amendment ledger: day-1 commitments share `committed_at = create_media_buy.confirmed_at`; mid-flight additions carry their own timestamps. When `create_media_buy.confirmed_at` is null for a provisional buy, sellers MUST omit `committed_metrics` until commitment. The first response that sets `confirmed_at` MAY include the initial committed-metrics set, and each such entry's `committed_at` MUST equal `confirmed_at`. The `missing_metrics` field on `get_media_buy_delivery` reconciles against this list, filtering to entries where `committed_at < reporting_period.end` (a metric committed mid-flight is only audited from its commitment timestamp forward). Sellers stamp the day-1 set on the `create_media_buy` response; mid-flight additions are appended via `update_media_buy` (append-only — sellers MUST reject attempts to modify or remove existing entries with `validation_error`, suggested code: `IMMUTABLE_FIELD`). Optional in v1; absence means the seller does not provide an audit-grade contract and `missing_metrics` falls back to the product's live `available_metrics` (a known audit gap — buyers SHOULD treat absence as 'no audit-grade contract' rather than 'clean delivery'). Each entry uses an explicit `scope` discriminator: `standard` for entries from the closed `available-metric.json` enum, `vendor` for vendor-defined metrics anchored on a BrandRef. The unified shape is symmetric with `missing_metrics` and `aggregated_totals.metric_aggregates` — same atomic unit `(scope, metric_id, qualifier)` across contract, diff, and delivery, so reconciliation collapses to a row-level join on the tuple. Replaces the parallel-array design that shipped briefly in #3510.",
            examples=[
                [
                    {
                        'scope': 'standard',
                        'metric_id': 'impressions',
                        'committed_at': '2026-04-29T10:53:00Z',
                    },
                    {
                        'scope': 'standard',
                        'metric_id': 'spend',
                        'committed_at': '2026-04-29T10:53:00Z',
                    },
                    {
                        'scope': 'standard',
                        'metric_id': 'completed_views',
                        'committed_at': '2026-04-29T10:53:00Z',
                    },
                    {
                        'scope': 'vendor',
                        'vendor': {'domain': 'attentionvendor.example'},
                        'metric_id': 'attention_units',
                        'committed_at': '2026-04-29T10:53:00Z',
                    },
                    {
                        'scope': 'standard',
                        'metric_id': 'viewable_rate',
                        'qualifier': {'viewability_standard': 'mrc'},
                        'committed_at': '2026-05-30T14:22:00Z',
                    },
                ]
            ],
            min_length=1,
        ),
    ] = None
    creative_assignments: Annotated[
        list[creative_assignment.CreativeAssignment] | None,
        Field(
            description='Creative assets assigned to this package, including the committed package-scoped rotation policy. Omitted rotation_mode reads as weighted for backward compatibility; all assignments resolve to one effective mode, and sequential positions are unique within each package-local group.'
        ),
    ] = None
    formats_to_provide: Annotated[
        list[product_format_declaration.ProductFormatDeclaration] | None,
        Field(
            description='Canonical creative contracts that the buyer must satisfy for this package. Each entry is a package-time snapshot of a selected Product.format_options declaration (or the equivalent declaration normalized from a direct format_kind + params selector) and MUST equal or narrow that product contract. Full declarations keep the requirement stable if the live product or publisher catalog later changes and remain usable when format_option_id is absent. Sellers SHOULD emit this field whenever additional creative coverage is required.',
            min_length=1,
        ),
    ] = None
    formats_pending: Annotated[
        list[product_format_declaration.ProductFormatDeclaration] | None,
        Field(
            description='The declarations from formats_to_provide that do not yet have creative coverage through sync_creatives or inline creative assignment. An empty emitted array means every required format is covered. Absence means readiness was not reported, so buyers MUST NOT infer full coverage from omission. Sellers SHOULD emit this field with formats_to_provide when returning current package readiness.'
        ),
    ] = None
    format_ids_to_provide: Annotated[
        list[format_id.FormatReferenceStructuredObject] | None,
        Field(
            deprecated=True,
            description='**DEPRECATED in 3.2.** Legacy named-format projection of formats_to_provide retained for older 3.x peers. New sellers emit canonical formats_to_provide declarations.',
        ),
    ] = None
    format_ids_pending: Annotated[
        list[format_id.FormatReferenceStructuredObject] | None,
        Field(
            deprecated=True,
            description='**DEPRECATED in 3.2.** Legacy named-format projection of formats_pending retained for older 3.x peers. New sellers emit canonical formats_pending declarations. An empty emitted array means every projected requirement is covered. Absence means legacy readiness was not reported and MUST NOT be interpreted as full coverage.',
        ),
    ] = None
    optimization_goals: Annotated[
        list[optimization_goal.OptimizationGoal] | None,
        Field(
            description='Optimization targets for this package. The seller optimizes delivery toward these goals in priority order. Common pattern: event goals (purchase, install) as primary targets at priority 1; metric goals (clicks, views) as secondary proxy signals at priority 2+.',
            min_length=1,
        ),
    ] = None
    start_time: Annotated[
        AwareDatetime | None,
        Field(
            description="Flight start date/time for this package in ISO 8601 format. When omitted, the package inherits the media buy's start_time. Sellers SHOULD always include the resolved value in responses, even when inherited."
        ),
    ] = None
    end_time: Annotated[
        AwareDatetime | None,
        Field(
            description="Flight end date/time for this package in ISO 8601 format. When omitted, the package inherits the media buy's end_time. Sellers SHOULD always include the resolved value in responses, even when inherited."
        ),
    ] = None
    paused: Annotated[
        bool | None,
        Field(
            description='Whether this package is paused by the buyer. Paused packages do not deliver impressions. Defaults to false.'
        ),
    ] = False
    canceled: Annotated[
        bool | None,
        Field(
            description='Whether this package has been canceled. Canceled packages stop delivery and cannot be reactivated. Defaults to false.'
        ),
    ] = False
    cancellation: Annotated[
        Cancellation | None,
        Field(description='Cancellation metadata. Present only when canceled is true.'),
    ] = None
    agency_estimate_number: Annotated[
        str | None,
        Field(
            description="Agency estimate or authorization number for this package. Echoed from the buyer's request. When present on the package, takes precedence over the media buy-level estimate number.",
            max_length=100,
        ),
    ] = None
    creative_deadline: Annotated[
        AwareDatetime | None,
        Field(
            description="ISO 8601 timestamp for creative upload or change deadline for this package. After this deadline, creative changes are rejected. When absent, the media buy's creative_deadline applies."
        ),
    ] = None
    context: Annotated[
        context_1.ContextObject | None,
        Field(
            description='Opaque package-level correlation data echoed unchanged in responses, webhooks, and read surfaces. Buyers targeting mixed seller populations SHOULD include a per-package correlation value here, commonly context.buyer_ref, so responses from legacy sellers that do not echo product_id can still be mapped back to the requested product or line item. Sellers MUST preserve this object unchanged and MUST NOT parse it for business logic.'
        ),
    ] = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var agency_estimate_number : str | None
var audience_evidence_selections : list[adcp.types.generated_poc.core.audience_evidence_selection.AudienceEvidenceSelection] | None
var bid_price : float | None
var bidding : adcp.types.generated_poc.core.bidding_policy.BiddingPolicy | None
var budget : float | None
var canceled : bool | None
var cancellation : adcp.types.generated_poc.core.package.Cancellation | None
var catalogs : list[adcp.types.generated_poc.core.catalog.Catalog] | None
var committed_metrics : list[adcp.types.generated_poc.core.committed_metric.CommittedMetric] | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var creative_assignments : list[adcp.types.generated_poc.core.creative_assignment.CreativeAssignment] | None
var creative_deadline : pydantic.types.AwareDatetime | None
var daily_budget_cap : float | None
var end_time : pydantic.types.AwareDatetime | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var format_ids : list[adcp.types.generated_poc.core.format_id.FormatReferenceStructuredObject] | None
var format_ids_pending : list[adcp.types.generated_poc.core.format_id.FormatReferenceStructuredObject] | None
var format_ids_to_provide : list[adcp.types.generated_poc.core.format_id.FormatReferenceStructuredObject] | None
var format_kind : adcp.types.generated_poc.core.canonical_format_kind.CanonicalFormatKind | None
var format_option_refs : list[adcp.types.generated_poc.core.format_option_ref.FormatOptionReference] | None
var formats_pending : list[adcp.types.generated_poc.core.product_format_declaration.ProductFormatDeclaration] | None
var formats_to_provide : list[adcp.types.generated_poc.core.product_format_declaration.ProductFormatDeclaration] | None
var impressions : float | None
var measurement_terms : adcp.types.generated_poc.core.measurement_terms.MeasurementTerms | None
var min_spend_target : float | None
var model_config
var optimization_goals : list[adcp.types.generated_poc.core.optimization_goal.OptimizationGoal] | None
var pacing : adcp.types.generated_poc.enums.pacing.Pacing | None
var package_id : str
var params : dict[str, typing.Any] | None
var paused : bool | None
var performance_standards : list[adcp.types.generated_poc.core.performance_standard.PerformanceStandard] | None
var price_breakdown : adcp.types.generated_poc.pricing_options.price_breakdown.PriceBreakdown | None
var pricing_option_id : str | None
var product_id : str | None
var start_time : pydantic.types.AwareDatetime | None
var targeting_overlay : adcp.types.generated_poc.core.targeting.TargetingOverlay | None
var targeting_resolution : adcp.types.generated_poc.core.package_targeting_resolution.PackageTargetingResolution | None
class MediaBuyPackage (**data: Any)
Expand source code
class Package(IndicatorBearingResourceState):
    model_config = ConfigDict(
        extra='allow',
    )
    indicator_types_evaluated: Annotated[
        list[IndicatorTypesEvaluatedEnum1] | None,
        Field(
            description='Indicator types covered by this snapshot. Required whenever indicators is present. Types omitted from this list remain unknown even when indicators is empty. Every returned indicator.type MUST appear in this list.',
            min_length=1,
        ),
    ] = None
    indicators: Annotated[
        list[Indicator1] | None,
        Field(
            description='Current seller assertions for the indicator types and publisher/placement coverage named by the sibling evaluation fields. Omitted means unknown or not evaluated. A present empty array means evaluated with no current assertion for indicator_types_evaluated in the evaluated scope.'
        ),
    ] = None
    package_id: Annotated[str, Field(description="Seller's package identifier")]
    product_id: Annotated[
        str | None,
        Field(
            description="Product identifier this package is purchased from. For packages created from an explicit create_media_buy package request, sellers MUST echo the request package's product_id on every response package object that represents that requested package."
        ),
    ] = None
    budget: Annotated[
        float | None,
        Field(
            description='Hard lifetime package spend cap denominated in media_buy.currency. In seller-optimized mode this is not a current allocation.',
            ge=0.0,
        ),
    ] = None
    min_spend_target: Annotated[
        float | None,
        Field(
            description='Accepted soft lifetime spend target for this package under seller-optimized allocation.',
            ge=0.0,
        ),
    ] = None
    daily_budget_cap: Annotated[
        float | None,
        Field(
            description='Current hard package spend ceiling per shared media-buy cap day, denominated in media_buy.currency. It is a subordinate ceiling, not a reserved allocation; media_buy.budget_cap_timezone defines the day boundary.',
            ge=0.0,
        ),
    ] = None
    currency: Annotated[
        str | None,
        Field(
            description='Legacy/readback package denomination for buys created outside the canonical 3.2 path. For AdCP-authored media buys this MUST equal media_buy.currency; canonical package budget and BiddingPolicy values always use media_buy.currency. Snapshot currency may still identify externally reported spend denomination.',
            pattern='^[A-Z]{3}$',
        ),
    ] = None
    bid_price: Annotated[
        float | None,
        Field(
            deprecated=True,
            description='DEPRECATED legacy bid representation. 3.2 sellers SHOULD normalize and echo package.bidding instead.',
            ge=0.0,
        ),
    ] = None
    bidding: Annotated[
        bidding_policy.BiddingPolicy | None,
        Field(
            description='Package-authored bidding override. `{automatic:true}` explicitly overrides media_buy.bidding with provider automatic delivery. Omitted when this package inherits; sellers MUST NOT copy an inherited block here. Monetary fields use media_buy.currency.'
        ),
    ] = None
    optimization_goals: Annotated[
        list[optimization_goal.OptimizationGoal] | None,
        Field(
            description='Current package objective functions. Currency-bearing execution controls are returned separately in package.bidding or inherited from media_buy.bidding.',
            min_length=1,
        ),
    ] = None
    format_ids: Annotated[
        list[format_id.FormatReferenceStructuredObject] | None,
        Field(
            deprecated=True,
            description='Deprecated in AdCP 3.2; removed in AdCP 4.0. Legacy named-format IDs supplied for this package on create_media_buy. Sellers SHOULD echo this field whenever the request included it, including dual-emission cases where another selector won precedence.',
            min_length=1,
        ),
    ] = None
    format_option_refs: Annotated[
        list[format_option_ref.FormatOptionReference] | None,
        Field(
            description='Structured 3.1+ format option references supplied for this package on create_media_buy. Sellers SHOULD echo this field whenever the request included it.',
            min_length=1,
        ),
    ] = None
    format_kind: Annotated[
        canonical_format_kind.CanonicalFormatKind | None,
        Field(
            description='Direct canonical selector supplied for this package on create_media_buy. Sellers SHOULD echo this field whenever the request included it, including informational-echo cases where another selector won precedence.'
        ),
    ] = None
    params: Annotated[
        dict[str, Any] | None,
        Field(
            description='Parameters for the direct canonical selector in `format_kind`, echoed from the create_media_buy request whenever the request included it. Requires `format_kind`.'
        ),
    ] = None
    impressions: Annotated[
        float | None,
        Field(description='Goal impression count for impression-based packages', ge=0.0),
    ] = None
    pacing: Annotated[
        pacing_1.Pacing | None,
        Field(
            description='Package-level pacing preference. Under seller-optimized allocation this is subordinate to the media-buy aggregate pacing.'
        ),
    ] = None
    targeting_overlay: Annotated[
        targeting.TargetingOverlay | None,
        Field(
            description='Complete effective targeting applied to this package, including configured-product targeting and the most recent package-specific overlay. Sellers SHOULD echo persisted targeting so buyers can verify stored state without replaying requests. Sellers MUST echo geo_places and geo_places_exclude whenever either was persisted, including the exact applied system_version and normalized values, so buyers can audit catalog-backed targeting. Sellers using placement, property-list, or collection-list targeting MUST include the committed inventory selection here. placement_selection mode default SHOULD resolve to mode selected with committed refs when enumerable.'
        ),
    ] = None
    targeting_resolution: Annotated[
        package_targeting_resolution.PackageTargetingResolution | None,
        Field(
            description='Execution details for accepted package targeting. Sellers MUST include targeting_resolution.demographics whenever demographic targeting was requested or applied; its applied predicate and execution fields report the effective booked state.'
        ),
    ] = None
    start_time: Annotated[
        AwareDatetime | None,
        Field(
            description='ISO 8601 flight start time for this package. Use to determine whether the package is within its scheduled flight before interpreting delivery status.'
        ),
    ] = None
    end_time: Annotated[
        AwareDatetime | None, Field(description='ISO 8601 flight end time for this package')
    ] = None
    paused: Annotated[
        bool | None, Field(description='Whether this package is currently paused by the buyer')
    ] = None
    canceled: Annotated[
        bool | None,
        Field(
            description='Whether this package has been canceled. Canceled packages stop delivery and cannot be reactivated.'
        ),
    ] = None
    cancellation: Annotated[
        Cancellation1 | None,
        Field(description='Cancellation metadata. Present only when canceled is true.'),
    ] = None
    creative_deadline: Annotated[
        AwareDatetime | None,
        Field(
            description="ISO 8601 timestamp for creative upload or change deadline for this package. After this deadline, creative changes are rejected. When absent, the media buy's creative_deadline applies."
        ),
    ] = None
    context: Annotated[
        context_1.ContextObject | None,
        Field(
            description='Opaque package-level correlation data echoed unchanged from the create_media_buy package request. Sellers MUST include persisted package context on read surfaces when the package was created through AdCP with context, so buyers can reconcile seller-assigned package_id values with their own line items; this is the legacy-safe fallback when an older seller did not echo product_id on the create response. Sellers MAY omit context for packages created outside AdCP or created without context. Sellers MUST NOT parse this object for business logic.'
        ),
    ] = None
    creative_approvals: Annotated[
        list[CreativeApproval] | None,
        Field(
            description='Approval status for each creative assigned to this package. Absent when no creatives have been assigned.'
        ),
    ] = None
    formats_to_provide: Annotated[
        list[product_format_declaration.ProductFormatDeclaration] | None,
        Field(
            description='The immutable canonical creative contracts established for this package at booking time. Each entry is the selected Product.format_options declaration, or the equivalent declaration normalized from a direct format_kind + params selector. Compare this full checklist with formats_pending to determine current creative readiness.',
            min_length=1,
        ),
    ] = None
    formats_pending: Annotated[
        list[product_format_declaration.ProductFormatDeclaration] | None,
        Field(
            description='Canonical package-time format declarations from formats_to_provide that do not yet have creative coverage through sync_creatives or inline creative assignment. Each entry preserves the full contract needed to select a compatible creative agent, including ID-less unique-kind and direct canonical selections. An empty emitted array means every formats_to_provide requirement is covered. Absence means readiness was not reported, so buyers MUST NOT infer full coverage from omission.'
        ),
    ] = None
    format_ids_to_provide: Annotated[
        list[format_id.FormatReferenceStructuredObject] | None,
        Field(
            deprecated=True,
            description='**DEPRECATED in 3.2.** Legacy named-format projection of formats_to_provide retained for older 3.x peers. New sellers emit canonical formats_to_provide declarations.',
        ),
    ] = None
    format_ids_pending: Annotated[
        list[format_id.FormatReferenceStructuredObject] | None,
        Field(
            deprecated=True,
            description='**DEPRECATED in 3.2.** Legacy named-format projection of formats_pending retained for older 3.x peers. New sellers emit canonical formats_pending declarations. An empty emitted array means every projected requirement is covered. Absence means legacy readiness was not reported and MUST NOT be interpreted as full coverage.',
        ),
    ] = None
    snapshot_unavailable_reason: Annotated[
        snapshot_unavailable_reason_1.SnapshotUnavailableReason | None,
        Field(
            description='Machine-readable reason the snapshot is omitted. Present only when include_snapshot was true and snapshot is unavailable for this package.'
        ),
    ] = None
    snapshot: Annotated[
        Snapshot | None,
        Field(
            description='Near-real-time delivery snapshot for this package. Only present when include_snapshot was true in the request. Represents the latest available entity-level stats from the platform — not billing-grade data.'
        ),
    ] = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.indicator_bearing.IndicatorBearingResourceState
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var bid_price : float | None
var bidding : adcp.types.generated_poc.core.bidding_policy.BiddingPolicy | None
var budget : float | None
var canceled : bool | None
var cancellation : adcp.types.generated_poc.media_buy.get_media_buys_response.Cancellation1 | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var creative_approvals : list[adcp.types.generated_poc.media_buy.get_media_buys_response.CreativeApproval] | None
var creative_deadline : pydantic.types.AwareDatetime | None
var currency : str | None
var daily_budget_cap : float | None
var end_time : pydantic.types.AwareDatetime | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var format_ids : list[adcp.types.generated_poc.core.format_id.FormatReferenceStructuredObject] | None
var format_ids_pending : list[adcp.types.generated_poc.core.format_id.FormatReferenceStructuredObject] | None
var format_ids_to_provide : list[adcp.types.generated_poc.core.format_id.FormatReferenceStructuredObject] | None
var format_kind : adcp.types.generated_poc.core.canonical_format_kind.CanonicalFormatKind | None
var format_option_refs : list[adcp.types.generated_poc.core.format_option_ref.FormatOptionReference] | None
var formats_pending : list[adcp.types.generated_poc.core.product_format_declaration.ProductFormatDeclaration] | None
var formats_to_provide : list[adcp.types.generated_poc.core.product_format_declaration.ProductFormatDeclaration] | None
var impressions : float | None
var indicator_types_evaluated : list[adcp.types.generated_poc.media_buy.get_media_buys_response.IndicatorTypesEvaluatedEnum1] | None
var indicators : list[adcp.types.generated_poc.media_buy.get_media_buys_response.Indicator1] | None
var min_spend_target : float | None
var model_config
var optimization_goals : list[adcp.types.generated_poc.core.optimization_goal.OptimizationGoal] | None
var pacing : adcp.types.generated_poc.enums.pacing.Pacing | None
var package_id : str
var params : dict[str, typing.Any] | None
var paused : bool | None
var product_id : str | None
var snapshot : adcp.types.generated_poc.media_buy.get_media_buys_response.Snapshot | None
var snapshot_unavailable_reason : adcp.types.generated_poc.enums.snapshot_unavailable_reason.SnapshotUnavailableReason | None
var start_time : pydantic.types.AwareDatetime | None
var targeting_overlay : adcp.types.generated_poc.core.targeting.TargetingOverlay | None
var targeting_resolution : adcp.types.generated_poc.core.package_targeting_resolution.PackageTargetingResolution | None
class Package (**data: Any)

Base class enforcing the primary canonical runtime boundary.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var agency_estimate_number : str | None
var audience_evidence_selections : list[adcp.types.generated_poc.core.audience_evidence_selection.AudienceEvidenceSelection] | None
var bid_price : float | None
var bidding : adcp.types.generated_poc.core.bidding_policy.BiddingPolicy | None
var budget : float | None
var canceled : bool | None
var cancellation : adcp.types.generated_poc.core.package.Cancellation | None
var catalogs : list[adcp.types.generated_poc.core.catalog.Catalog] | None
var committed_metrics : list[adcp.types.generated_poc.core.committed_metric.CommittedMetric] | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var creative_assignments : list[adcp.types.generated_poc.core.creative_assignment.CreativeAssignment] | None
var creative_deadline : pydantic.types.AwareDatetime | None
var daily_budget_cap : float | None
var end_time : pydantic.types.AwareDatetime | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var format_kind : adcp.types.generated_poc.core.canonical_format_kind.CanonicalFormatKind | None
var format_option_refs : list[adcp.types.generated_poc.core.format_option_ref.FormatOptionReference] | None
var formats_pending : list[adcp.types.generated_poc.core.product_format_declaration.ProductFormatDeclaration] | None
var formats_to_provide : list[adcp.types.generated_poc.core.product_format_declaration.ProductFormatDeclaration] | None
var impressions : float | None
var measurement_terms : adcp.types.generated_poc.core.measurement_terms.MeasurementTerms | None
var min_spend_target : float | None
var model_config
var optimization_goals : list[adcp.types.generated_poc.core.optimization_goal.OptimizationGoal] | None
var pacing : adcp.types.generated_poc.enums.pacing.Pacing | None
var package_id : str
var params : dict[str, typing.Any] | None
var paused : bool | None
var performance_standards : list[adcp.types.generated_poc.core.performance_standard.PerformanceStandard] | None
var price_breakdown : adcp.types.generated_poc.pricing_options.price_breakdown.PriceBreakdown | None
var pricing_option_id : str | None
var product_id : str | None
var start_time : pydantic.types.AwareDatetime | None
var targeting_overlay : adcp.types.generated_poc.core.targeting.TargetingOverlay | None
var targeting_resolution : adcp.types.generated_poc.core.package_targeting_resolution.PackageTargetingResolution | None

Inherited members

class LegacyPackageRequest (**data: Any)
Expand source code
class PackageRequest(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    product_id: Annotated[
        str,
        Field(
            description="Opaque configured product ID returned by get_products. Selecting it accepts the product's disclosed targeting_resolution, pricing, forecast assumptions, and terms. Sellers MUST echo this value on every response package object that represents this requested package."
        ),
    ]
    format_ids: Annotated[
        list[format_id_1.FormatReferenceStructuredObject] | None,
        Field(
            deprecated=True,
            description='Deprecated in AdCP 3.2; removed in AdCP 4.0. Legacy named-format selector retained for older 3.x peers. New buyers MUST NOT emit this field. Sellers MUST normalize every entry through the canonical mapping path before product satisfaction checks; an entry that cannot be normalized is rejected with `UNSUPPORTED_FEATURE` before any equivalence check. When this field coexists with `format_option_refs` or `format_kind` plus `params`, sellers MUST compare the product option sets selected by each resolved route. Legacy parameter compatibility follows the asymmetric v2-narrows-v1 relation defined by canonical formats, not raw object equality. Different format shapes, selected option sets, or incompatible dimensions are rejected with `CONFLICTING_SELECTORS`; sellers MUST NOT silently ignore the legacy projection. Equivalent dual emission remains valid during the 3.x compatibility window. If omitted and no canonical selector is present, all formats supported by the product are active.',
            min_length=1,
        ),
    ] = None
    format_option_refs: Annotated[
        list[format_option_ref_1.FormatOptionReference] | None,
        Field(
            description='Canonical 3.2 format-option selector. Each reference matches one target product `format_options[]` entry. Publisher-backed options match `{ scope: "publisher", publisher_domain, format_option_id }`; product-local options match `{ scope: "product", format_option_id }`. Sellers reject unresolved options with `UNSUPPORTED_FEATURE` and a field path to the failing entry before comparing co-present routes. New buyers MUST use this route by itself and MUST NOT dual-emit either a direct canonical selector or deprecated `format_ids`. Receivers handling older 3.x multi-route requests MUST resolve every present route, require each route to select the same product option set, and reject disagreement with `CONFLICTING_SELECTORS` before treating `format_option_refs` as authoritative.',
            min_length=1,
        ),
    ] = None
    format_kind: Annotated[
        canonical_format_kind.CanonicalFormatKind | None,
        Field(
            description='Canonical 3.2 direct selector. Names the canonical format shape this package targets when the buyer is not selecting a published `format_option_ref`. Pair with `params` for dimensions, duration, codecs, or other constraints. New buyers MUST NOT combine this route with `format_option_refs` or deprecated `format_ids`. Receivers handling older 3.x multi-route requests MUST equivalence-check every present route before applying precedence and reject disagreement with `CONFLICTING_SELECTORS`. Product satisfaction is directional: broad `{ format_kind: "image" }` does not satisfy a fixed-size product declaration.'
        ),
    ] = None
    params: Annotated[
        dict[str, Any] | None,
        Field(
            description="Parameters for the direct canonical selector in `format_kind`. Shape follows the selected canonical's parameter vocabulary: dimensions (`width`, `height`, `sizes`), duration (`duration_ms_exact`, `duration_ms_range`), codecs, asset-source and slot narrowing, or other canonical-specific constraints. Requires `format_kind`. For fixed-size image selectors, `width` and `height` MUST co-occur; a selector containing only one dimension is schema-invalid. New buyers omit `params` when selecting by `format_option_refs` or `format_ids`; older multi-route requests are accepted only when every route selects the same product option set."
        ),
    ] = None
    budget: Annotated[
        float | None,
        Field(
            description="Hard lifetime spend cap for this package in the media buy's currency. Required in fixed allocation mode. Optional in seller-optimized mode; when omitted, the package is bounded by the shared total_budget and any other package constraints. In seller-optimized mode this is a ceiling, not a reserved or current allocation.",
            ge=0.0,
        ),
    ] = None
    min_spend_target: Annotated[
        float | None,
        Field(
            description="Soft lifetime spend target for this package in the media buy's currency. Only valid with seller-optimized budget allocation. The seller SHOULD attempt to deliver at least this amount before allocating incremental spend elsewhere, but inventory, policy, optimization targets, or other delivery constraints may prevent it. This is not a billing guarantee. Must not exceed the package budget when both are present; sellers MUST reject with `INVALID_REQUEST` when this constraint is violated.",
            ge=0.0,
        ),
    ] = None
    pacing: pacing_1.Pacing | None = None
    pricing_option_id: Annotated[
        str,
        Field(
            description="ID of the selected pricing option from the product's pricing_options array"
        ),
    ]
    bid_price: Annotated[
        float | None,
        Field(
            deprecated=True,
            description='DEPRECATED in 3.2 and removed in the next major. Use bidding.bid_amount or bidding.max_bid. Legacy normalization: selected pricing_option.max_bid=true maps to bidding.max_bid; otherwise maps to bidding.bid_amount. A package MUST NOT supply both representations.',
            ge=0.0,
        ),
    ] = None
    bidding: Annotated[
        bidding_policy.BiddingPolicy | None,
        Field(
            description='Package-authored bidding policy. This complete block replaces, rather than field-merges with, any media-buy bidding policy for this package. `{automatic:true}` explicitly overrides a media-buy policy with provider automatic bidding; omission inherits the complete media-buy block. Monetary fields use the media-buy currency, while the selected pricing option supplies only the auction unit and MUST declare that same currency. Sellers MUST reject a new bidding block combined with legacy bid_price or legacy monetary optimization-goal targets on the same effective package with AMBIGUOUS_BIDDING_POLICY.'
        ),
    ] = None
    impressions: Annotated[
        float | None, Field(description='Impression goal for this package', ge=0.0)
    ] = None
    daily_budget_cap: Annotated[
        float | None,
        Field(
            description="Optional hard package daily spend ceiling in the media-buy currency. It is subordinate, not a reserved allocation; package caps need not sum to the aggregate cap. Uses the media buy's cap timezone. Requires advertised package budget-capping scope; otherwise rejected with UNSUPPORTED_FEATURE.",
            ge=0.0,
        ),
    ] = None
    start_time: Annotated[
        AwareDatetime | None,
        Field(
            description="Flight start date/time for this package in ISO 8601 format. When omitted, the package inherits the media buy's start_time. Must fall within the media buy's date range."
        ),
    ] = None
    end_time: Annotated[
        AwareDatetime | None,
        Field(
            description="Flight end date/time for this package in ISO 8601 format. When omitted, the package inherits the media buy's end_time. Must fall within the media buy's date range."
        ),
    ] = None
    paused: Annotated[
        bool | None,
        Field(
            description='Whether this package should be created in a paused state. Paused packages do not deliver impressions. Defaults to false.'
        ),
    ] = False
    catalogs: Annotated[
        list[catalog.Catalog] | None,
        Field(
            description='Catalogs this package promotes. Each catalog MUST have a distinct type (e.g., one product catalog, one store catalog). This constraint is enforced at the application level — sellers MUST reject requests containing multiple catalogs of the same type with a validation_error. Makes the package catalog-driven: one budget envelope, platform optimizes across items.'
        ),
    ] = None
    optimization_goals: Annotated[
        list[optimization_goal.OptimizationGoal] | None,
        Field(
            description='Optimization targets for this package. The seller optimizes delivery toward these goals in priority order. Common pattern: event goals (purchase, install) as primary targets at priority 1; metric goals (clicks, views) as secondary proxy signals at priority 2+.',
            min_length=1,
        ),
    ] = None
    targeting_overlay: Annotated[
        targeting.TargetingOverlay | None,
        Field(
            description="Optional package-specific targeting that further constrains targeting already bound to the configured product. It cannot broaden or remove configured-product targeting. Fields supplied here MUST be declared in the product's overlay_support unless they were already accepted during discovery. The one fixed-inventory restatement exception is placement_selection equal to the product's complete, explicitly enumerated mode: included placement set: that set is an inherent exact match across discovery, create, and update and does not require overlay_support.placement_selection; partial selection still requires a selectable product. Opaque property_list and collection_list references have no equivalent exception because their membership can change independently and cannot be proven equal from the product wire representation. The seller applies the intersection exactly or rejects the package; package readback echoes the complete effective targeting. A supported value with no current inventory returns PRODUCT_UNAVAILABLE rather than a silent substitute or reprice."
        ),
    ] = None
    audience_evidence_requirements: Annotated[
        audience_evidence_requirements_1.AudienceEvidenceRequirements | None,
        Field(
            description='Buyer policy that the selected product and constructed package MUST satisfy using product audience evidence. This remains planning and suitability evidence, not a targeting instruction. Sellers MUST reject an unsatisfied required policy rather than silently drop it, and a confirmed package MUST include every evidence snapshot used to satisfy this policy in audience_evidence_selections with decision_use package_construction.'
        ),
    ] = None
    audience_evidence_pins: Annotated[
        list[audience_evidence_pin.AudienceEvidencePin] | None,
        Field(
            description="Exact immutable evidence snapshots selected by the buyer during discovery. The seller MUST match evidence_id, snapshot_id, version, and content_digest against one published snapshot and MUST reject catalog mutation, snapshot reuse, missing snapshots, or substitutions. Every accepted pin MUST be echoed in the confirmed package's audience_evidence_selections with decision_use package_construction.",
            min_length=1,
        ),
    ] = None
    measurement_terms: Annotated[
        measurement_terms_1.MeasurementTerms | None,
        Field(
            description="Buyer's proposed billing measurement and makegood terms. Overrides product defaults. Seller accepts (echoed on confirmed package), rejects with TERMS_REJECTED, or adjusts. When absent, product's measurement_terms apply."
        ),
    ] = None
    performance_standards: Annotated[
        list[performance_standard.PerformanceStandard] | None,
        Field(
            description="Buyer's proposed performance standards for this package. Overrides product defaults. Seller accepts, rejects with TERMS_REJECTED, or adjusts. When absent, product's performance_standards apply.",
            min_length=1,
        ),
    ] = None
    committed_metrics: Annotated[
        list[CommittedMetrics] | None,
        Field(
            description="Buyer's proposed reporting contract for this package — the metrics the buyer wants the seller to commit to populating in delivery reports. Same negotiation pattern as `measurement_terms` and `performance_standards`: seller accepts (echoes on confirmed package with `committed_at` stamped), rejects with `TERMS_REJECTED` (with explanation of which entries were unworkable), or normalizes (echoes a different but compatible list — buyer can accept by retrying with the normalized terms). When absent, the seller decides what to commit based on the product's `available_metrics` and the buyer's `required_metrics` filter on `get_products`. Each entry uses an explicit `scope` discriminator (`standard` or `vendor`) and identifies the metric — request-side entries do NOT carry `committed_at`; that timestamp is stamped by the seller on accept. Constraints on what the buyer MAY propose: each `scope: standard` entry's `metric_id` MUST be in the product's `available_metrics`, and each `scope: vendor` entry's `(vendor, metric_id)` MUST appear in the product's `vendor_metrics` — sellers SHOULD reject with `TERMS_REJECTED` and reference the offending entry when the proposal exceeds product capability.",
            min_length=1,
        ),
    ] = None
    creative_assignments: Annotated[
        list[creative_assignment.CreativeAssignment] | None,
        Field(
            description='Assign existing library creatives to this package with optional rotation, grouping, weights, and placement targeting. rotation_mode is package-scoped: omission resolves to weighted, and every assignment MUST resolve to the same effective mode. In sequential mode, sequence_position MUST be unique within each package-local group. Sellers reject conflicts with VALIDATION_ERROR before creating the package.',
            min_length=1,
        ),
    ] = None
    creatives: Annotated[
        Sequence[Creative] | None,
        Field(
            description="Upload creative assets inline and assign to this package. Native localization is not accepted on this path; use sync_creatives before assigning the library creative. When the seller also advertises creative.has_creative_library: true, these creatives enter the seller's creative library and can be reused by creative_id while retained; inline-only sellers may store them as package-scoped assets. Use creative_assignments instead for existing library creatives.",
            max_length=100,
            min_length=1,
        ),
    ] = None
    agency_estimate_number: Annotated[
        str | None,
        Field(
            description='Agency estimate or authorization number for this package. Overrides the media buy-level estimate number when different packages correspond to different agency estimates (e.g., different stations or flights within the same buy).',
            max_length=100,
        ),
    ] = None
    context: Annotated[
        context_1.ContextObject | None,
        Field(
            description='Opaque package-level correlation data echoed unchanged in the package response, webhooks, and read surfaces. Buyers targeting mixed seller populations SHOULD include a per-package correlation value here, commonly context_1.buyer_ref, so responses from legacy sellers that do not echo product_id can still be mapped back to the requested product or line item. Do not use deprecated top-level buyer_ref for v3 correlation.'
        ),
    ] = None
    ext: ext_1.ExtensionObject | None = None

    @model_validator(mode='after')
    def _validate_format_params(self) -> PackageRequest:
        if self.params is not None and self.format_kind is None:
            raise ValueError('params requires format_kind')
        if self.params is not None and self.format_kind == 'image':
            if ('width' in self.params) != ('height' in self.params):
                raise ValueError('image params width and height must co-occur')
        return self

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var agency_estimate_number : str | None
var audience_evidence_pins : list[adcp.types.generated_poc.core.audience_evidence_pin.AudienceEvidencePin] | None
var audience_evidence_requirements : adcp.types.generated_poc.core.audience_evidence_requirements.AudienceEvidenceRequirements | None
var bidding : adcp.types.generated_poc.core.bidding_policy.BiddingPolicy | None
var budget : float | None
var catalogs : list[adcp.types.generated_poc.core.catalog.Catalog] | None
var committed_metrics : list[adcp.types.generated_poc.media_buy.package_request.CommittedMetrics] | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var creative_assignments : list[adcp.types.generated_poc.core.creative_assignment.CreativeAssignment] | None
var creatives : collections.abc.Sequence[adcp.types.generated_poc.media_buy.package_request.Creative] | None
var daily_budget_cap : float | None
var end_time : pydantic.types.AwareDatetime | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var format_kind : adcp.types.generated_poc.core.canonical_format_kind.CanonicalFormatKind | None
var format_option_refs : list[adcp.types.generated_poc.core.format_option_ref.FormatOptionReference] | None
var impressions : float | None
var measurement_terms : adcp.types.generated_poc.core.measurement_terms.MeasurementTerms | None
var min_spend_target : float | None
var model_config
var optimization_goals : list[adcp.types.generated_poc.core.optimization_goal.OptimizationGoal] | None
var pacing : adcp.types.generated_poc.enums.pacing.Pacing | None
var params : dict[str, typing.Any] | None
var paused : bool | None
var performance_standards : list[adcp.types.generated_poc.core.performance_standard.PerformanceStandard] | None
var pricing_option_id : str
var product_id : str
var start_time : pydantic.types.AwareDatetime | None
var targeting_overlay : adcp.types.generated_poc.core.targeting.TargetingOverlay | None

Instance variables

var adcp_major_version : int | None
Expand source code
def __get__(self, obj: BaseModel | None, obj_type: type[BaseModel] | None = None) -> Any:
    if obj is None:
        if self.wrapped_property is not None:
            return self.wrapped_property.__get__(None, obj_type)
        raise AttributeError(self.field_name)

    warnings.warn(self.msg, DeprecationWarning, stacklevel=2)

    if self.wrapped_property is not None:
        return self.wrapped_property.__get__(obj, obj_type)
    return obj.__dict__[self.field_name]

Read-only data descriptor used to emit a runtime deprecation warning before accessing a deprecated field.

Attributes
-----=
msg
The deprecation message to be emitted.
wrapped_property
The property instance if the deprecated field is a computed field, or None.
field_name
The name of the field being deprecated.
var bid_price : float | None
Expand source code
def __get__(self, obj: BaseModel | None, obj_type: type[BaseModel] | None = None) -> Any:
    if obj is None:
        if self.wrapped_property is not None:
            return self.wrapped_property.__get__(None, obj_type)
        raise AttributeError(self.field_name)

    warnings.warn(self.msg, DeprecationWarning, stacklevel=2)

    if self.wrapped_property is not None:
        return self.wrapped_property.__get__(obj, obj_type)
    return obj.__dict__[self.field_name]

Read-only data descriptor used to emit a runtime deprecation warning before accessing a deprecated field.

Attributes
-----=
msg
The deprecation message to be emitted.
wrapped_property
The property instance if the deprecated field is a computed field, or None.
field_name
The name of the field being deprecated.
var format_ids : list[adcp.types.generated_poc.core.format_id.FormatReferenceStructuredObject] | None
Expand source code
def __get__(self, obj: BaseModel | None, obj_type: type[BaseModel] | None = None) -> Any:
    if obj is None:
        if self.wrapped_property is not None:
            return self.wrapped_property.__get__(None, obj_type)
        raise AttributeError(self.field_name)

    warnings.warn(self.msg, DeprecationWarning, stacklevel=2)

    if self.wrapped_property is not None:
        return self.wrapped_property.__get__(obj, obj_type)
    return obj.__dict__[self.field_name]

Read-only data descriptor used to emit a runtime deprecation warning before accessing a deprecated field.

Attributes
-----=
msg
The deprecation message to be emitted.
wrapped_property
The property instance if the deprecated field is a computed field, or None.
field_name
The name of the field being deprecated.
class PackageRequest (**data: Any)
Expand source code
class PackageRequest(_PackageRequestBase):
    """Canonical package request preserving beta.3 selector constraints."""

    @model_validator(mode="after")
    def _validate_format_params(self) -> PackageRequest:
        if self.params is not None and self.format_kind is None:
            raise ValueError("params requires format_kind")
        if self.params is not None and self.format_kind == "image":
            if ("width" in self.params) != ("height" in self.params):
                raise ValueError("image params width and height must co-occur")
        return self

Canonical package request preserving beta.3 selector constraints.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var model_config

Inherited members

class PackageSignalTargeting (root: RootModelRootType = PydanticUndefined, **data)
Expand source code
class PackageSignalTargeting(
    RootModel[PackageSignalTargeting5 | PackageSignalTargeting6 | PackageSignalTargeting7]
):
    root: Annotated[
        PackageSignalTargeting5 | PackageSignalTargeting6 | PackageSignalTargeting7,
        Field(
            description="Buy-time selection of one seller-offered signal inside a package signal targeting group. The signal_ref uses scope 'product' for a product-local signal option, scope 'data_provider' for a signal defined in a data provider's published adagents.json signals[], or scope 'signal_source' for a source-native signal that is not published in adagents.json signals[]. The selected product's inline Product.signal_targeting_options, get_signals feed when inline options are omitted, and signal_targeting_rules define buy-time eligibility. Inclusion and exclusion are controlled by the parent group operator: use operator 'any' to include users matching the signal expression and operator 'none' to exclude users matching the signal expression. For binary signals, value MUST be true; do not use value=false for exclusion inside signal_targeting_groups. Use audience_include/audience_exclude only for buyer-managed first-party audiences registered through sync_audiences.",
            title='Package Signal Targeting',
        ),
    ]
    def __getattr__(self, name: str) -> Any:
        """Proxy attribute access to the wrapped type."""
        if name.startswith('_'):
            raise AttributeError(name)
        return getattr(self.root, name)

Usage Documentation

RootModel and Custom Root Types

A Pydantic BaseModel for the root object of the model.

Attributes
-----=
root
The root object of the model.
__pydantic_root_model__
Whether the model is a RootModel.
__pydantic_private__
Private fields in the model.
__pydantic_extra__
Extra fields in the model.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • pydantic.root_model.RootModel[Union[PackageSignalTargeting5, PackageSignalTargeting6, PackageSignalTargeting7]]
  • pydantic.root_model.RootModel
  • pydantic.main.BaseModel
  • typing.Generic

Class variables

var model_config
var root : adcp.types.generated_poc.core.package_signal_targeting.PackageSignalTargeting5 | adcp.types.generated_poc.core.package_signal_targeting.PackageSignalTargeting6 | adcp.types.generated_poc.core.package_signal_targeting.PackageSignalTargeting7
class PackageSignalTargetingGroup (**data: Any)
Expand source code
class PackageSignalTargetingGroup(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    operator: Annotated[
        Operator,
        Field(
            description="How to evaluate the signals in this group. 'any' is an OR include group. 'none' is an exclusion group equivalent to NOT (A OR B OR C)."
        ),
    ]
    signals: Annotated[
        list[package_signal_targeting.PackageSignalTargeting],
        Field(
            description='Signal targeting entries evaluated by this group. Each entry uses the package signal targeting shape, including signal_ref, value expression, and optional pricing, execution-handle, or activation fields.',
            min_length=1,
        ),
    ]

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var model_config
var operator : adcp.types.generated_poc.core.package_signal_targeting_group.Operator
var signals : list[adcp.types.generated_poc.core.package_signal_targeting.PackageSignalTargeting]

Inherited members

class PackageSignalTargetingGroups (**data: Any)
Expand source code
class PackageSignalTargetingGroups(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    operator: Annotated[
        Literal['all'],
        Field(
            description="Groups-level operator. Required even though v1 only supports 'all': every child group must be satisfied."
        ),
    ] = 'all'
    groups: Annotated[
        list[package_signal_targeting_group.PackageSignalTargetingGroup],
        Field(
            description="Signal targeting groups to evaluate. Use operator 'any' for include groups and 'none' for exclusion groups.",
            min_length=1,
        ),
    ]

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var groups : list[adcp.types.generated_poc.core.package_signal_targeting_group.PackageSignalTargetingGroup]
var model_config
var operator : Literal['all']

Inherited members

class LegacyPackageUpdate (**data: Any)
Expand source code
class PackageUpdate(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    package_id: Annotated[str, Field(description="Seller's ID of package to update")]
    budget: Annotated[
        float | None,
        Field(
            description='Updated hard spend cap for this package in the media-buy currency. Every selected pricing option in an AdCP-authored media buy MUST declare that same currency. In seller-optimized mode a number changes the package ceiling and null removes it so only the shared total and other constraints bound the package. null is invalid when the resulting allocation mode is fixed.',
            ge=0.0,
        ),
    ] = None
    min_spend_target: Annotated[
        float | None,
        Field(
            description='Updated soft lifetime spend target for this package. A number is valid only for seller-optimized allocation and must not exceed the resulting package budget when one exists. null removes the target. Sellers MUST validate the complete post-update state atomically.',
            ge=0.0,
        ),
    ] = None
    pacing: pacing_1.Pacing | None = None
    bid_price: Annotated[
        float | None,
        Field(
            deprecated=True,
            description='DEPRECATED in 3.2 and removed in the next major. Use bidding. A package update MUST NOT supply both a non-null bidding object and bid_price. During migration, bidding:null MAY accompany bid_price to clear the canonical block and set the legacy representation atomically.',
            ge=0.0,
        ),
    ] = None
    bidding: Annotated[
        bidding_policy.BiddingPolicy | None,
        Field(
            description='Replace the complete package-authored bidding policy. An object replaces any prior package block and remains a complete override of the media-buy default. `{automatic:true}` explicitly selects provider automatic bidding at package scope. null clears the package-authored block so the package inherits media-buy bidding; if the media-buy block is also absent, provider automatic delivery applies. Monetary fields use the media-buy currency and require the package pricing option to declare that currency. During legacy migration, null MAY accompany bid_price or monetary optimization-goal targets; only a non-null canonical bidding object conflicts with those representations.'
        ),
    ] = None
    impressions: Annotated[
        float | None, Field(description='Updated impression goal for this package', ge=0.0)
    ] = None
    daily_budget_cap: Annotated[
        float | None,
        Field(
            description="Replace this package's hard daily cap; null removes it. Numeric changes apply immediately with current-day package spend counted. A cap below that spend pauses the package for the day; the aggregate cap remains independently binding.",
            ge=0.0,
        ),
    ] = None
    start_time: Annotated[
        AwareDatetime | None,
        Field(
            description="Updated flight start date/time for this package in ISO 8601 format. Must fall within the media buy's date range."
        ),
    ] = None
    end_time: Annotated[
        AwareDatetime | None,
        Field(
            description="Updated flight end date/time for this package in ISO 8601 format. Must fall within the media buy's date range."
        ),
    ] = None
    paused: Annotated[
        bool | None,
        Field(description='Pause/resume specific package (true = paused, false = active)'),
    ] = None
    canceled: Annotated[
        Literal[True] | None,
        Field(
            description='Cancel this specific package. Cancellation is irreversible — canceled packages stop delivery and cannot be reactivated. When true, package cancellation takes precedence over sibling fields on this package: the seller applies only canceled and cancellation_reason for this package and SHOULD return a structured warning naming ignored sibling fields. Root fields and other package updates still participate in the same atomic update when root canceled is absent. Sellers MAY reject with NOT_CANCELLABLE.'
        ),
    ] = None
    cancellation_reason: Annotated[
        str | None, Field(description='Reason for canceling this package.', max_length=500)
    ] = None
    catalogs: Annotated[
        list[catalog.Catalog] | None,
        Field(
            description='Replace the catalogs this package promotes. Uses replacement semantics — the provided array replaces the current list. Omit to leave catalogs unchanged.',
            min_length=1,
        ),
    ] = None
    optimization_goals: Annotated[
        list[optimization_goal.OptimizationGoal] | None,
        Field(
            description='Replace all optimization goals for this package. Uses replacement semantics — omit to leave goals unchanged.',
            min_length=1,
        ),
    ] = None
    targeting_overlay: Annotated[
        targeting.TargetingOverlay | None,
        Field(
            description="Complete effective targeting overlay to apply to this package. On update, this replaces the package's current effective targeting, including values originally accepted through configured-product selection; omit the field to leave targeting unchanged. Every replacement must remain executable by the product. Sellers reject unsupported or partially applicable changes and use REQUOTE_REQUIRED when a change, including a broader inventory set, falls outside the priced envelope. placement_selection is purchased-inventory targeting; mode default restores the product default, and successful readback echoes the committed selected set when enumerable. If the replacement removes a placement referenced by an existing creative assignment, the seller MUST reject the update unless the same atomic package mutation supplies a compatible complete creative_assignments replacement. Sellers MUST NOT silently delete assignments or retain orphan placement refs."
        ),
    ] = None
    keyword_targets_add: Annotated[
        list[KeywordTargetsAddItem] | None,
        Field(
            description='Keyword targets to add or update on this package. Upserts by (keyword, match_type) identity: if the pair already exists, its bid_price is updated; if not, a new keyword target is added. Use targeting_overlay.keyword_targets in create_media_buy to set the initial list.',
            min_length=1,
        ),
    ] = None
    keyword_targets_remove: Annotated[
        list[KeywordTargetsRemoveItem] | None,
        Field(
            description='Keyword targets to remove from this package. Removes matching (keyword, match_type) pairs. If a specified pair is not present, sellers SHOULD treat it as a no-op for that entry.',
            min_length=1,
        ),
    ] = None
    negative_keywords_add: Annotated[
        list[NegativeKeywordsAddItem] | None,
        Field(
            description='Negative keywords to add to this package. Appends to the existing negative keyword list — does not replace it. If a keyword+match_type pair already exists, sellers SHOULD treat it as a no-op for that entry. Use targeting_overlay.negative_keywords in create_media_buy to set the initial list.',
            min_length=1,
        ),
    ] = None
    negative_keywords_remove: Annotated[
        list[NegativeKeywordsRemoveItem] | None,
        Field(
            description='Negative keywords to remove from this package. Removes matching keyword+match_type pairs from the existing list. If a specified pair is not present, sellers SHOULD treat it as a no-op for that entry.',
            min_length=1,
        ),
    ] = None
    creative_assignments: Annotated[
        list[creative_assignment.CreativeAssignment] | None,
        Field(
            description='Replace creative assignments for this package with optional rotation, grouping, weights, and placement routing. Uses replacement semantics - omit to leave assignments unchanged. rotation_mode is package-scoped: omission resolves to weighted, and every assignment MUST resolve to the same effective mode. In sequential mode, sequence_position MUST be unique within each package-local group. Sellers reject conflicts with VALIDATION_ERROR before mutation. When the same mutation narrows targeting_overlay.placement_selection, this complete replacement MUST remove or reroute every assignment reference that would otherwise be orphaned; the seller validates both changes atomically.'
        ),
    ] = None
    creatives: Annotated[
        list[Creative] | None,
        Field(
            description="Replace this package's inline creative assets. Native localization is not accepted on this path; use sync_creatives before assigning the library creative. When the seller also advertises creative.has_creative_library: true, new inline creatives enter the seller's creative library and can be reused by creative_id while retained; inline-only sellers may store them as package-scoped assets. Use creative_assignments instead for existing library creatives.",
            max_length=100,
            min_length=1,
        ),
    ] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var bidding : adcp.types.generated_poc.core.bidding_policy.BiddingPolicy | None
var budget : float | None
var canceled : Literal[True] | None
var cancellation_reason : str | None
var catalogs : list[adcp.types.generated_poc.core.catalog.Catalog] | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var creative_assignments : list[adcp.types.generated_poc.core.creative_assignment.CreativeAssignment] | None
var creatives : list[adcp.types.generated_poc.media_buy.package_update.Creative] | None
var daily_budget_cap : float | None
var end_time : pydantic.types.AwareDatetime | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var impressions : float | None
var keyword_targets_add : list[adcp.types.generated_poc.media_buy.package_update.KeywordTargetsAddItem] | None
var keyword_targets_remove : list[adcp.types.generated_poc.media_buy.package_update.KeywordTargetsRemoveItem] | None
var min_spend_target : float | None
var model_config
var negative_keywords_add : list[adcp.types.generated_poc.media_buy.package_update.NegativeKeywordsAddItem] | None
var negative_keywords_remove : list[adcp.types.generated_poc.media_buy.package_update.NegativeKeywordsRemoveItem] | None
var optimization_goals : list[adcp.types.generated_poc.core.optimization_goal.OptimizationGoal] | None
var pacing : adcp.types.generated_poc.enums.pacing.Pacing | None
var package_id : str
var paused : bool | None
var start_time : pydantic.types.AwareDatetime | None
var targeting_overlay : adcp.types.generated_poc.core.targeting.TargetingOverlay | None

Instance variables

var bid_price : float | None
Expand source code
def __get__(self, obj: BaseModel | None, obj_type: type[BaseModel] | None = None) -> Any:
    if obj is None:
        if self.wrapped_property is not None:
            return self.wrapped_property.__get__(None, obj_type)
        raise AttributeError(self.field_name)

    warnings.warn(self.msg, DeprecationWarning, stacklevel=2)

    if self.wrapped_property is not None:
        return self.wrapped_property.__get__(obj, obj_type)
    return obj.__dict__[self.field_name]

Read-only data descriptor used to emit a runtime deprecation warning before accessing a deprecated field.

Attributes
-----=
msg
The deprecation message to be emitted.
wrapped_property
The property instance if the deprecated field is a computed field, or None.
field_name
The name of the field being deprecated.

Inherited members

class PaginationRequest (**data: Any)
Expand source code
class PaginationRequest(AdCPBaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    max_results: Annotated[
        int | None, Field(description='Maximum number of items to return per page', ge=1, le=100)
    ] = 50
    cursor: Annotated[
        str | None,
        Field(description='Opaque cursor from a previous response to fetch the next page'),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var cursor : str | None
var max_results : int | None
var model_config

Inherited members

class PixelTrackerAsset (**data: Any)
Expand source code
class PixelTrackerAsset(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    asset_type: Annotated[
        Literal['pixel_tracker'],
        Field(
            description='Discriminator identifying this as a renderer-fired pixel tracker asset. See /schemas/creative/asset-types for the registry.'
        ),
    ] = 'pixel_tracker'
    event: Annotated[
        Event,
        Field(
            description="Which event this tracker fires on. Event enum mirrors IAB OpenRTB Native 1.2 event-tracker registry (event types 1, 2, 3, 4, 500); the events themselves are generic web-pixel measurement events that apply to any renderer:\n- `impression` (IAB type 1) — fires when the ad is served. Covers both `imptrackers[]` and `jstracker` from the IAB shape, distinguished by `method`.\n- `viewable_mrc_50` (IAB type 2) — IAB MRC viewable, 50% pixels for ≥1 second.\n- `viewable_mrc_100` (IAB type 3) — IAB MRC viewable, 100% pixels for ≥1 second.\n- `viewable_video_50` (IAB type 4) — video-specific viewable, 50% pixels for ≥2 seconds with audio on. On video_hosted; ignored on image/html5.\n- `audible_video_complete` (IAB type 500) — video reached 100% completion with audio on. Distinct from `viewable_video_50` (50% pixels + 2s threshold) — this is the full-completion audible-view event. Meaningful on non-VAST video formats (Meta Reels, YouTube Shorts, TikTok Spark) where audible-complete is a measured event but VAST `<TrackingEvents>` isn't the wire format; VAST formats use `vast_tracker` with `vast_event: complete` plus a separate audible tracker instead.\n- `click` — fires when the user clicks the creative (`link.clicktrackers[]`).\n- `custom` — adopter-defined event for anything not in the standardized enum. MUST also set `custom_event_name`. Reserved for IAB Native event types 555+ (exchange-specific) and any vendor-defined event not yet promoted to a first-class enum value."
        ),
    ]
    method: Annotated[
        Method | None,
        Field(
            description="How the tracker URL is invoked at serve time:\n- `img` — fired as an image pixel (HTTP GET with `<img>`-like semantics; no JS execution)\n- `js` — fired as a script include (renderer evaluates the URL's response as JavaScript)\n\nMatches IAB OpenRTB Native 1.2 method enum (1=img, 2=js). `js` MUST only be used by sellers whose renderer supports JavaScript trackers; sellers without JS-tracker support MUST reject `method: js` declarations at sync_creatives time with `CREATIVE_REJECTED` carrying the reason."
        ),
    ] = Method.img
    url: Annotated[
        str,
        Field(
            description="Tracker URL fired when `event` occurs. May carry AdCP universal macros (e.g., `{MEDIA_BUY_ID}`, `{CREATIVE_ID}`, `{CACHEBUSTER}`); the seller's renderer URL-encodes substituted values at serve time. See docs/creative/universal-macros.mdx."
        ),
    ]
    custom_event_name: Annotated[
        str | None,
        Field(
            description='REQUIRED when `event` is `custom`; otherwise MUST be absent. Adopter-defined event name. Sellers without registered handling for a given custom_event_name MUST silently no-op (do not fire) rather than reject — custom events are forward-compatible probes.'
        ),
    ] = None
    provenance: Annotated[
        provenance_1.Provenance | None,
        Field(
            description='Provenance metadata for this asset, overrides manifest-level provenance.'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var asset_type : Literal['pixel_tracker']
var custom_event_name : str | None
var event : adcp.types.generated_poc.core.assets.pixel_tracker_asset.Event
var method : adcp.types.generated_poc.core.assets.pixel_tracker_asset.Method | None
var model_config
var provenance : adcp.types.generated_poc.core.provenance.Provenance | None
var url : str

Inherited members

class LegacyPlacement (**data: Any)
Expand source code
class Placement(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    kind: Annotated[
        Kind,
        Field(
            description="Placement structure discriminator. `publisher_ref` identifies a placement by `{publisher_domain, placement_id}` and resolves public metadata from the named publisher's adagents.json placement declarations; `seller_inline` identifies buyer-facing placement metadata defined inline by the sales agent (still in the named publisher namespace when `publisher_domain` is present, or the seller's own namespace in legacy single-publisher contexts)."
        ),
    ]
    placement_id: Annotated[
        str,
        Field(
            description="Placement identifier in the publisher namespace. When `publisher_domain` is present, this matches a placement ID in that publisher's adagents.json catalog or a seller-defined inline placement in that publisher namespace. Buyers use this with `publisher_domain` in `creative_assignments[].placement_refs`; legacy `creative_assignments[].placement_ids` strings are only unambiguous in single-publisher contexts."
        ),
    ]
    publisher_domain: Annotated[
        str | None,
        Field(
            description='Publisher domain whose adagents.json placement declarations define this placement. Required for `kind: "publisher_ref"`. Omitted only for `kind: "seller_inline"` in legacy single-publisher seller contexts where the seller agent\'s own publisher domain is the namespace.',
            pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$',
        ),
    ] = None
    name: Annotated[
        str | None,
        Field(
            description='Human-readable name for the placement (e.g., \'Homepage Banner\', \'Article Sidebar\'). Required for `kind: "seller_inline"`. May be omitted for publisher-referenced placements because buyers resolve the name from the publisher declaration identified by `{publisher_domain, placement_id}`.'
        ),
    ] = None
    description: Annotated[
        str | None, Field(description='Detailed description of where and how the placement appears')
    ] = None
    mode: Annotated[
        Mode,
        Field(
            description="Required product-level relationship to this placement. targetable means the buyer may include the publisher-scoped ref in targeting_overlay.placement_selection; a creative may be routed there only after it is purchased. included means fixed product inventory: it cannot be independently selected, but across discovery, create, and update a selected request exactly equal to the product's complete included placement set is an inherent restatement and may be echoed on the package without overlay_support.placement_selection. A product containing any included placement MUST NOT declare overlay_support.placement_selection; partial selection requires a separately selectable product configuration. During the migration window ending 2026-11-25, buyers MAY tolerate legacy products that omit mode and treat them as targetable; after that date buyers SHOULD fail closed."
        ),
    ]
    tags: Annotated[
        list[str] | None,
        Field(
            description="Optional tags for grouping placements within a product (e.g., 'homepage', 'native', 'premium'). When the placement_id comes from the publisher registry, these should align with the registry tags unless the product is narrowing scope."
        ),
    ] = None
    format_ids: Annotated[
        Sequence[format_id.FormatReferenceStructuredObject] | None,
        Field(
            deprecated=True,
            description='Deprecated in AdCP 3.2; removed in AdCP 4.0. Legacy named-format placement narrowing. Can include concrete, template, or parameterized format IDs. When present on a product placement, this field narrows the product-level `format_ids` contract and MUST NOT introduce formats the product does not accept. Use canonical `format_options`.',
            min_length=1,
        ),
    ] = None
    format_options: Annotated[
        list[product_format_declaration.ProductFormatDeclaration] | None,
        Field(
            description="Canonical seller-side narrowing for this product placement. When present, these declarations are intersected with the product-level format_options and MUST NOT introduce a format outside that product upper bound. For kind publisher_ref, buyers MUST also resolve {publisher_domain, placement_id} in the publisher's adagents.json and intersect the publisher catalog constraint: use the public placement's format_options when present (resolving bare format_option_id references against same-file top-level formats[]), otherwise use applicable top-level formats[] scoped to that placement's properties. Omitting this inline field removes only the seller-inline layer; it does not bypass a publisher placement or property-scoped narrowing. The placement inherits the full product-level set only when no applicable publisher catalog narrowing exists. Unresolved publisher placement or format-option references fail closed. Locale policy participates in the same intersection: when the product policy is absent, a placement may introduce any concrete policy as a narrowing of the unconstrained option; when both are present, every placement accepted_language_range must be contained by a product range under RFC 4647 Basic Filtering (`fr-CA` narrows `fr`; `fr` does not narrow `fr-CA`). Buyers compute effective locale eligibility independently for each placement. Any effective locale-constrained route is canonical-only and has no projecting product or placement format_id.",
            min_length=1,
        ),
    ] = None
    video_placement_types: Annotated[
        list[video_placement_type.VideoPlacementType] | None,
        Field(
            description='Declared video placement types for this product placement, using IAB Tech Lab/OpenRTB 2.6 video.plcmt definitions with AdCP-native names. Most concrete placements SHOULD declare a single value; aggregate placements MAY declare multiple values. This is seller-declared discovery metadata, not independent verification of inventory quality or delivery context.',
            min_length=1,
        ),
    ] = None
    audio_distribution_types: Annotated[
        list[audio_distribution_type.AudioDistributionType] | None,
        Field(
            description='Declared audio distribution types for this product placement, using IAB Tech Lab/OpenRTB 2.6 audio.feed definitions with AdCP-native names. Most concrete placements SHOULD declare a single value; aggregate placements MAY declare multiple values. This is seller-declared discovery metadata, not independent verification of inventory quality or delivery context.',
            min_length=1,
        ),
    ] = None
    sponsored_placement_types: Annotated[
        list[sponsored_placement_type.SponsoredPlacementType] | None,
        Field(
            description='Declared sponsored-placement types for this product placement, distinguishing where the catalog-driven retail-media placement renders on the retailer surface. Most concrete placements SHOULD declare a single value; aggregate placements MAY declare multiple values. This is seller-declared discovery metadata, not independent verification of inventory quality or delivery context.',
            min_length=1,
        ),
    ] = None
    social_placement_surfaces: Annotated[
        list[social_placement_surface.SocialPlacementSurface] | None,
        Field(
            description='Declared social-placement surfaces for this product placement, distinguishing the in-app surface where the social placement renders. Most concrete placements SHOULD declare a single value; aggregate placements MAY declare multiple values. This is seller-declared discovery metadata, not independent verification of inventory quality or delivery context.',
            min_length=1,
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var audio_distribution_types : list[adcp.types.generated_poc.enums.audio_distribution_type.AudioDistributionType] | None
var description : str | None
var format_ids : collections.abc.Sequence[adcp.types.generated_poc.core.format_id.FormatReferenceStructuredObject] | None
var format_options : list[adcp.types.generated_poc.core.product_format_declaration.ProductFormatDeclaration] | None
var kind : adcp.types.generated_poc.core.placement.Kind
var mode : adcp.types.generated_poc.core.placement.Mode
var model_config
var name : str | None
var placement_id : str
var publisher_domain : str | None
var social_placement_surfaces : list[adcp.types.generated_poc.enums.social_placement_surface.SocialPlacementSurface] | None
var sponsored_placement_types : list[adcp.types.generated_poc.enums.sponsored_placement_type.SponsoredPlacementType] | None
var tags : list[str] | None
var video_placement_types : list[adcp.types.generated_poc.enums.video_placement_type.VideoPlacementType] | None
class Placement (**data: Any)

Base class enforcing the primary canonical runtime boundary.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var audio_distribution_types : list[adcp.types.generated_poc.enums.audio_distribution_type.AudioDistributionType] | None
var description : str | None
var format_options : list[Format] | None
var kind : adcp.types.generated_poc.core.placement.Kind
var mode : adcp.types.generated_poc.core.placement.Mode
var model_config
var name : str | None
var placement_id : str
var publisher_domain : str | None
var social_placement_surfaces : list[adcp.types.generated_poc.enums.social_placement_surface.SocialPlacementSurface] | None
var sponsored_placement_types : list[adcp.types.generated_poc.enums.sponsored_placement_type.SponsoredPlacementType] | None
var tags : list[str] | None
var video_placement_types : list[adcp.types.generated_poc.enums.video_placement_type.VideoPlacementType] | None

Inherited members

class PlacementPresentationDocument (**data: Any)
Expand source code
class PlacementPresentationDocument(AdCPBaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    schema_version: Literal['1.0'] = '1.0'
    canvas: Canvas
    creative_slot: Annotated[
        CreativeSlot,
        Field(
            description='Rectangle into which the selected creative render is fitted and clipped without changing its manifest or renderer.'
        ),
    ]
    decorations: Annotated[
        list[BoxDecoration | TextDecoration | ImageDecoration] | None, Field(max_length=100)
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var canvas : adcp.types.generated_poc.core.placement_presentation.Canvas
var creative_slot : adcp.types.generated_poc.core.placement_presentation.CreativeSlot
var decorations : list[adcp.types.generated_poc.core.placement_presentation.BoxDecoration | adcp.types.generated_poc.core.placement_presentation.TextDecoration | adcp.types.generated_poc.core.placement_presentation.ImageDecoration] | None
var model_config
var schema_version : Literal['1.0']

Inherited members

class PlacementPresentationReference (**data: Any)
Expand source code
class PlacementPresentationReference(AdCPBaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    uri: Annotated[
        AnyUrl,
        Field(
            description='Publisher-controlled HTTPS URL for the presentation metadata. Consumers MUST apply the same SSRF, redirect, response-size, timeout, and DNS-rebinding protections used for format_schema fetches.'
        ),
    ]
    digest: Annotated[
        str,
        Field(
            description='SHA-256 content digest. Consumers cache by uri@digest and MUST fail closed on a digest mismatch.',
            pattern='^sha256:[a-f0-9]{64}$',
        ),
    ]
    media_type: Annotated[
        Literal['application/vnd.adcp.placement-presentation+json'],
        Field(description='Media type of the referenced declarative presentation document.'),
    ] = 'application/vnd.adcp.placement-presentation+json'
    schema_version: Annotated[
        Literal['1.0'],
        Field(
            description='Version of /schemas/core/placement-presentation.json used to validate and compose the referenced document.'
        ),
    ] = '1.0'

    @field_validator('uri')
    @classmethod
    def _require_https_uri(cls, value: AnyUrl) -> AnyUrl:
        if value.scheme != 'https':
            raise ValueError('uri must use https')
        return value

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var digest : str
var media_type : Literal['application/vnd.adcp.placement-presentation+json']
var model_config
var schema_version : Literal['1.0']
var uri : pydantic.networks.AnyUrl

Inherited members

class PlacementReference (**data: Any)
Expand source code
class PlacementReference(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    publisher_domain: Annotated[
        str | None,
        Field(
            description="Domain where the adagents.json declaring this placement is hosted. Omitted only for legacy single-publisher seller contexts where the seller agent's own publisher domain is the namespace.",
            pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$',
        ),
    ] = None
    placement_id: Annotated[
        str,
        Field(
            description="Placement ID from the publisher's adagents.json placement catalog, or an inline seller-defined placement ID interpreted within the same publisher namespace."
        ),
    ]

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var model_config
var placement_id : str
var publisher_domain : str | None

Inherited members

class Policy (**data: Any)
Expand source code
class Policy(PolicySummary):
    """Full governance policy including policy text and calibration exemplars."""

    policy: str
    guidance: str | None = None
    exemplars: PolicyExemplars | None = None
    ext: dict[str, Any] | None = None

Full governance policy including policy text and calibration exemplars.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var exemplarsPolicyExemplars | None
var ext : dict[str, typing.Any] | None
var guidance : str | None
var model_config
var policy : str
class PolicyExemplar (**data: Any)
Expand source code
class PolicyExemplar(BaseModel):
    """A pass/fail scenario used to calibrate governance agent interpretation."""

    model_config = ConfigDict(extra="allow")

    scenario: str
    explanation: str

A pass/fail scenario used to calibrate governance agent interpretation.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • pydantic.main.BaseModel

Class variables

var explanation : str
var model_config
var scenario : str
class PolicyExemplars (**data: Any)
Expand source code
class PolicyExemplars(BaseModel):
    """Collection of pass/fail exemplars for a policy."""

    model_config = ConfigDict(extra="allow")

    pass_: list[PolicyExemplar] = Field(default_factory=list, alias="pass")
    fail: list[PolicyExemplar] = Field(default_factory=list)

Collection of pass/fail exemplars for a policy.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • pydantic.main.BaseModel

Class variables

var fail : list[PolicyExemplar]
var model_config
var pass_ : list[PolicyExemplar]
class PolicyHistory (**data: Any)
Expand source code
class PolicyHistory(BaseModel):
    """Edit history for a policy."""

    model_config = ConfigDict(extra="allow")

    policy_id: str
    total: int
    revisions: list[PolicyRevision] = Field(default_factory=list)

Edit history for a policy.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • pydantic.main.BaseModel

Class variables

var model_config
var policy_id : str
var revisions : list[PolicyRevision]
var total : int
class PolicyRevision (**data: Any)
Expand source code
class PolicyRevision(BaseModel):
    """A single revision in a policy's edit history."""

    model_config = ConfigDict(extra="allow")

    revision_number: int
    editor_name: str
    edit_summary: str
    is_rollback: bool
    rolled_back_to: int | None = None
    created_at: str

A single revision in a policy's edit history.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • pydantic.main.BaseModel

Class variables

var created_at : str
var edit_summary : str
var editor_name : str
var is_rollback : bool
var model_config
var revision_number : int
var rolled_back_to : int | None
class PolicySummary (**data: Any)
Expand source code
class PolicySummary(BaseModel):
    """Summary of a governance policy from the registry."""

    model_config = ConfigDict(extra="allow", populate_by_name=True)

    policy_id: str
    version: str
    name: str
    description: str | None = None
    category: str
    enforcement: str
    jurisdictions: list[str] = Field(default_factory=list)
    region_aliases: dict[str, list[str]] = Field(default_factory=dict)
    verticals: list[str] = Field(default_factory=list)
    channels: list[str] | None = None
    governance_domains: list[str] = Field(default_factory=list)
    effective_date: str | None = None
    sunset_date: str | None = None
    source_url: str | None = None
    source_name: str | None = None
    source_type: str | None = None
    review_status: str | None = None
    created_at: str | None = None
    updated_at: str | None = None

Summary of a governance policy from the registry.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • pydantic.main.BaseModel

Subclasses

Class variables

var category : str
var channels : list[str] | None
var created_at : str | None
var description : str | None
var effective_date : str | None
var enforcement : str
var governance_domains : list[str]
var jurisdictions : list[str]
var model_config
var name : str
var policy_id : str
var region_aliases : dict[str, list[str]]
var review_status : str | None
var source_name : str | None
var source_type : str | None
var source_url : str | None
var sunset_date : str | None
var updated_at : str | None
var version : str
var verticals : list[str]
class PreparedWebhook (url: str,
idempotency_key: str,
body: bytes,
extra_headers: Mapping[str, str] = <factory>)
Expand source code
@dataclass(frozen=True)
class PreparedWebhook:
    """Immutable webhook request prepared for durable outbox storage.

    The serialized body and idempotency key are bound once, before the
    outbox transaction commits. Workers may then replay this object under a
    fresh RFC 9421 signature without regenerating timestamps or JSON bytes.
    """

    url: str
    idempotency_key: str
    body: bytes
    extra_headers: Mapping[str, str] = field(default_factory=dict)

Immutable webhook request prepared for durable outbox storage.

The serialized body and idempotency key are bound once, before the outbox transaction commits. Workers may then replay this object under a fresh RFC 9421 signature without regenerating timestamps or JSON bytes.

Instance variables

var body : bytes
var extra_headers : Mapping[str, str]
var idempotency_key : str
var url : str
class LegacyPreviewCreativeRequest (**data: Any)
Expand source code
class PreviewCreativeRequest(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    request_type: Annotated[
        RequestType,
        Field(
            description="Preview mode. 'single' previews one creative manifest. 'batch' previews multiple creatives in one call. 'variant' replays a post-flight variant by ID."
        ),
    ]
    creative_manifest: Annotated[
        creative_manifest_1.CreativeManifest | None,
        Field(
            description='Complete creative manifest with all required assets for the format. In single mode, provide exactly one of creative_manifest or creative_id. Also accepted per item in batch mode.'
        ),
    ] = None
    target_capability_id: Annotated[
        str | None,
        Field(
            description="Canonical preview-operation selector. Identifies one get_adcp_capabilities creative.supported_formats[].capability_id entry whose operations contains preview. In single mode it selects the renderer for this request; in batch mode it is the default for items that omit their own target_capability_id. When omitted, the agent MAY resolve the renderer only if exactly one advertised preview capability satisfies the manifest's canonical declaration; zero matches or multiple matches MUST be rejected with FORMAT_NOT_SUPPORTED rather than choosing nondeterministically. Mutually exclusive with deprecated format_id.",
            pattern='^[a-zA-Z0-9_-]+$',
        ),
    ] = None
    format_id: Annotated[
        format_id_1.FormatReferenceStructuredObject | None,
        Field(
            deprecated=True,
            description='**DEPRECATED in 3.2.** Legacy named-format preview route. New requests select an advertised preview renderer with target_capability_id and carry portable format identity in creative_manifest_1.format_kind plus optional creative_manifest_1.format_option_ref.',
        ),
    ] = None
    inputs: Annotated[
        list[Input] | None,
        Field(
            description='Array of input sets for generating multiple preview variants. Each input set defines macros and context values for one preview rendering. Used in single mode.',
            min_length=1,
        ),
    ] = None
    template_id: Annotated[
        str | None,
        Field(description='Specific template ID for custom format rendering. Used in single mode.'),
    ] = None
    quality: Annotated[
        creative_quality.CreativeQuality | None,
        Field(
            description="Render quality. 'draft' produces fast, lower-fidelity renderings. 'production' produces full-quality renderings. In batch mode, sets the default for all requests (individual items can override)."
        ),
    ] = None
    output_format: Annotated[
        preview_output_format.PreviewOutputFormat | None,
        Field(
            description="Output format. 'url' returns preview_url (iframe-embeddable URL), 'html' returns preview_html (raw HTML). In batch mode, sets the default for all requests (individual items can override). Default: 'url'."
        ),
    ] = preview_output_format.PreviewOutputFormat.url
    item_limit: Annotated[
        int | None,
        Field(
            description='Maximum number of catalog items to render per preview variant. Used in single mode. Creative agents SHOULD default to a reasonable sample when omitted and the catalog is large.',
            ge=1,
        ),
    ] = None
    requests: Annotated[
        list[Request] | None,
        Field(
            description="Array of preview requests (1-50 items). Required when request_type is 'batch'. Each item follows the single request structure.",
            max_length=50,
            min_length=1,
        ),
    ] = None
    variant_id: Annotated[
        str | None,
        Field(
            description="Platform-assigned variant identifier from get_creative_delivery response. Required when request_type is 'variant'."
        ),
    ] = None
    creative_id: Annotated[
        str | None,
        Field(
            description='Creative-library identifier. In single mode, previews the stored canonical creative without requiring the caller to reconstruct its manifest. Also available as context in variant mode.'
        ),
    ] = None
    allow_async: Annotated[
        bool | None,
        Field(
            description="Opt in to an asynchronous preview response. When true, the creative agent MAY return status 'submitted' with a task_id only when rendering has been handed to a queue or external renderer and will continue after the request connection is released. Active processing on an open connection uses working progress instead. The buyer polls get_task_status for completion. When false or absent, the agent MUST return a synchronous preview response or a terminal protocol error; it MUST NOT return the submitted shape. This field applies to preview_creative only; build_creative already defines its own async lifecycle."
        ),
    ] = False
    push_notification_config: Annotated[
        push_notification_config_1.PushNotificationConfig | None,
        Field(
            description='Optional webhook configuration for terminal completion/failure notifications when allow_async is true and preview_creative returns a submitted task envelope. Submitted tasks remain pollable through get_task_status whether or not this field is present. If the agent accepts this configuration and returns submitted, it MUST deliver at least the terminal notification; if it cannot honor the webhook, it MUST return a structured error. Presence of this field alone MUST NOT cause asynchronous execution.'
        ),
    ] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var allow_async : bool | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var creative_id : str | None
var creative_manifest : adcp.types.generated_poc.core.creative_manifest.CreativeManifest | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var format_id : adcp.types.generated_poc.core.format_id.FormatReferenceStructuredObject | None
var inputs : list[adcp.types.generated_poc.creative.preview_creative_request.Input] | None
var item_limit : int | None
var model_config
var output_format : adcp.types.generated_poc.enums.preview_output_format.PreviewOutputFormat | None
var push_notification_config : adcp.types.generated_poc.core.push_notification_config.PushNotificationConfig | None
var quality : adcp.types.generated_poc.enums.creative_quality.CreativeQuality | None
var request_type : adcp.types.generated_poc.creative.preview_creative_request.RequestType
var requests : list[adcp.types.generated_poc.creative.preview_creative_request.Request] | None
var target_capability_id : str | None
var template_id : str | None
var variant_id : str | None

Inherited members

class LegacyPreviewCreativeResponse1 (**data: Any)
Expand source code
class PreviewCreativeResponse1(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    response_type: Literal['single'] = 'single'
    previews: Annotated[list[Preview], Field(min_length=1)]
    quality_used: creative_quality_1.CreativeQuality | None = None
    interactive_url: AnyUrl | None = None
    expires_at: AwareDatetime | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var expires_at : pydantic.types.AwareDatetime | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var interactive_url : pydantic.networks.AnyUrl | None
var model_config
var previews : list[adcp.types.generated_poc.creative.preview_creative_response.Preview]
var quality_used : adcp.types.generated_poc.enums.creative_quality.CreativeQuality | None
var response_type : Literal['single']
class LegacyPreviewCreativeSingleResponse (**data: Any)
Expand source code
class PreviewCreativeResponse1(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    response_type: Literal['single'] = 'single'
    previews: Annotated[list[Preview], Field(min_length=1)]
    quality_used: creative_quality_1.CreativeQuality | None = None
    interactive_url: AnyUrl | None = None
    expires_at: AwareDatetime | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var expires_at : pydantic.types.AwareDatetime | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var interactive_url : pydantic.networks.AnyUrl | None
var model_config
var previews : list[adcp.types.generated_poc.creative.preview_creative_response.Preview]
var quality_used : adcp.types.generated_poc.enums.creative_quality.CreativeQuality | None
var response_type : Literal['single']

Inherited members

class LegacyPreviewCreativeBatchResponse (**data: Any)
Expand source code
class PreviewCreativeResponse2(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    response_type: Literal['batch'] = 'batch'
    results: Annotated[list[Result], Field(min_length=1)]
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config
var response_type : Literal['batch']
var results : list[adcp.types.generated_poc.creative.preview_creative_response.Result]
class LegacyPreviewCreativeResponse2 (**data: Any)
Expand source code
class PreviewCreativeResponse2(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    response_type: Literal['batch'] = 'batch'
    results: Annotated[list[Result], Field(min_length=1)]
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config
var response_type : Literal['batch']
var results : list[adcp.types.generated_poc.creative.preview_creative_response.Result]

Inherited members

class LegacyPreviewCreativeResponse3 (**data: Any)
Expand source code
class PreviewCreativeResponse3(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    response_type: Literal['variant'] = 'variant'
    variant_id: str
    creative_id: str | None = None
    previews: Annotated[list[Preview3], Field(min_length=1)]
    manifest: creative_manifest_1.CreativeManifest | None = None
    expires_at: AwareDatetime | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var creative_id : str | None
var expires_at : pydantic.types.AwareDatetime | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var manifest : adcp.types.generated_poc.core.creative_manifest.CreativeManifest | None
var model_config
var previews : list[adcp.types.generated_poc.creative.preview_creative_response.Preview3]
var response_type : Literal['variant']
var variant_id : str
class LegacyPreviewCreativeVariantResponse (**data: Any)
Expand source code
class PreviewCreativeResponse3(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    response_type: Literal['variant'] = 'variant'
    variant_id: str
    creative_id: str | None = None
    previews: Annotated[list[Preview3], Field(min_length=1)]
    manifest: creative_manifest_1.CreativeManifest | None = None
    expires_at: AwareDatetime | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var creative_id : str | None
var expires_at : pydantic.types.AwareDatetime | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var manifest : adcp.types.generated_poc.core.creative_manifest.CreativeManifest | None
var model_config
var previews : list[adcp.types.generated_poc.creative.preview_creative_response.Preview3]
var response_type : Literal['variant']
var variant_id : str

Inherited members

class UrlPreviewRender (**data: Any)
Expand source code
class PreviewRender1(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    render_id: Annotated[
        str, Field(description='Unique identifier for this rendered piece within the variant')
    ]
    output_format: Annotated[
        Literal['url'], Field(description='Discriminator indicating preview_url is provided')
    ] = 'url'
    preview_url: Annotated[
        AnyUrl,
        Field(
            description='Untrusted URL to an HTML page that renders this piece. Consumers MUST load it only in a cross-origin iframe with an empty sandbox token set and a caller-enforced restrictive CSP. Provider embedding metadata is advisory and MUST NOT loosen that policy.'
        ),
    ]
    role: Annotated[
        str,
        Field(
            description="Semantic role of this rendered piece. Use 'primary' for main content, 'companion' for associated banners, descriptive strings for device variants or custom roles."
        ),
    ]
    dimensions: Annotated[
        Dimensions | None, Field(description='Dimensions for this rendered piece')
    ] = None
    embedding: Annotated[
        Embedding | None,
        Field(description='Optional security and embedding metadata for safe iframe integration'),
    ] = None
    renderer: Annotated[
        preview_renderer_metadata.PreviewRendererMetadata | None,
        Field(
            description='Optional renderer implementation and safety metadata for audit and reproducibility. Authority is still resolved from capability discovery and placement delegation.'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var dimensions : adcp.types.generated_poc.creative.preview_render.Dimensions | None
var embedding : adcp.types.generated_poc.creative.preview_render.Embedding | None
var model_config
var output_format : Literal['url']
var preview_url : pydantic.networks.AnyUrl
var render_id : str
var renderer : adcp.types.generated_poc.core.preview_renderer_metadata.PreviewRendererMetadata | None
var role : str

Inherited members

class HtmlPreviewRender (**data: Any)
Expand source code
class PreviewRender2(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    render_id: Annotated[
        str, Field(description='Unique identifier for this rendered piece within the variant')
    ]
    output_format: Annotated[
        Literal['html'], Field(description='Discriminator indicating preview_html is provided')
    ] = 'html'
    preview_html: Annotated[
        str,
        Field(
            description='Untrusted HTML. Consumers MUST NOT inject it into the host DOM. Render only as iframe srcdoc with an empty sandbox token set and a caller-enforced restrictive CSP; provider embedding metadata is advisory and MUST NOT loosen that policy.'
        ),
    ]
    role: Annotated[
        str,
        Field(
            description="Semantic role of this rendered piece. Use 'primary' for main content, 'companion' for associated banners, descriptive strings for device variants or custom roles."
        ),
    ]
    dimensions: Annotated[
        Dimensions | None, Field(description='Dimensions for this rendered piece')
    ] = None
    embedding: Annotated[
        Embedding | None, Field(description='Optional security and embedding metadata')
    ] = None
    renderer: Annotated[
        preview_renderer_metadata.PreviewRendererMetadata | None,
        Field(
            description='Optional renderer implementation and safety metadata for audit and reproducibility. Authority is still resolved from capability discovery and placement delegation.'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var dimensions : adcp.types.generated_poc.creative.preview_render.Dimensions | None
var embedding : adcp.types.generated_poc.creative.preview_render.Embedding | None
var model_config
var output_format : Literal['html']
var preview_html : str
var render_id : str
var renderer : adcp.types.generated_poc.core.preview_renderer_metadata.PreviewRendererMetadata | None
var role : str

Inherited members

class BothPreviewRender (**data: Any)
Expand source code
class PreviewRender3(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    render_id: Annotated[
        str, Field(description='Unique identifier for this rendered piece within the variant')
    ]
    output_format: Annotated[
        Literal['both'],
        Field(
            description='Discriminator indicating both preview_url and preview_html are provided'
        ),
    ] = 'both'
    preview_url: Annotated[
        AnyUrl,
        Field(
            description='Untrusted URL to an HTML page that renders this piece. Consumers MUST load it only in a cross-origin iframe with an empty sandbox token set and a caller-enforced restrictive CSP. Provider embedding metadata is advisory and MUST NOT loosen that policy.'
        ),
    ]
    preview_html: Annotated[
        str,
        Field(
            description='Untrusted HTML. Consumers MUST NOT inject it into the host DOM. Render only as iframe srcdoc with an empty sandbox token set and a caller-enforced restrictive CSP; provider embedding metadata is advisory and MUST NOT loosen that policy.'
        ),
    ]
    role: Annotated[
        str,
        Field(
            description="Semantic role of this rendered piece. Use 'primary' for main content, 'companion' for associated banners, descriptive strings for device variants or custom roles."
        ),
    ]
    dimensions: Annotated[
        Dimensions | None, Field(description='Dimensions for this rendered piece')
    ] = None
    embedding: Annotated[
        Embedding | None,
        Field(description='Optional security and embedding metadata for safe iframe integration'),
    ] = None
    renderer: Annotated[
        preview_renderer_metadata.PreviewRendererMetadata | None,
        Field(
            description='Optional renderer implementation and safety metadata for audit and reproducibility. Authority is still resolved from capability discovery and placement delegation.'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var dimensions : adcp.types.generated_poc.creative.preview_render.Dimensions | None
var embedding : adcp.types.generated_poc.creative.preview_render.Embedding | None
var model_config
var output_format : Literal['both']
var preview_html : str
var preview_url : pydantic.networks.AnyUrl
var render_id : str
var renderer : adcp.types.generated_poc.core.preview_renderer_metadata.PreviewRendererMetadata | None
var role : str

Inherited members

class PreviewRendererMetadata (**data: Any)
Expand source code
class PreviewRendererMetadata(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    renderer_id: Annotated[
        str, Field(description='Stable implementation identifier.', min_length=1)
    ]
    version: Annotated[
        str,
        Field(
            description='Exact semantic version of the renderer implementation.',
            pattern='^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$',
        ),
    ]
    export: Annotated[
        str,
        Field(
            description='Renderer export or entry-point name used for this render.', min_length=1
        ),
    ]
    rendering_origin: Annotated[
        RenderingOrigin,
        Field(
            description='Informational implementation origin copied from the selected route. It does not grant authority.'
        ),
    ]
    tracking_suppressed: Annotated[
        bool,
        Field(
            description='True only when the produced output cannot initiate impression, click, billing, conversion, viewability, or asset-fetch side effects. Renderers that retain any remote asset URL or navigation MUST emit false.'
        ),
    ]

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var export : str
var model_config
var renderer_id : str
var rendering_origin : adcp.types.generated_poc.core.preview_renderer_metadata.RenderingOrigin
var tracking_suppressed : bool
var version : str

Inherited members

class PriceGuidance (**data: Any)
Expand source code
class PriceGuidance(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    p25: Annotated[
        float | None, Field(description='25th percentile of recent winning bids', ge=0.0)
    ] = None
    p50: Annotated[float | None, Field(description='Median of recent winning bids', ge=0.0)] = None
    p75: Annotated[
        float | None, Field(description='75th percentile of recent winning bids', ge=0.0)
    ] = None
    p90: Annotated[
        float | None, Field(description='90th percentile of recent winning bids', ge=0.0)
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var model_config
var p25 : float | None
var p50 : float | None
var p75 : float | None
var p90 : float | None

Inherited members

class PricingCurrency (root: RootModelRootType = PydanticUndefined, **data)
Expand source code
class PricingCurrency(RootModel[str]):
    root: Annotated[
        str,
        Field(
            description="ISO 4217 currency code (e.g., 'USD', 'EUR', 'GBP')", pattern='^[A-Z]{3}$'
        ),
    ]

Usage Documentation

RootModel and Custom Root Types

A Pydantic BaseModel for the root object of the model.

Attributes
-----=
root
The root object of the model.
__pydantic_root_model__
Whether the model is a RootModel.
__pydantic_private__
Private fields in the model.
__pydantic_extra__
Extra fields in the model.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • pydantic.root_model.RootModel[str]
  • pydantic.root_model.RootModel
  • pydantic.main.BaseModel
  • typing.Generic

Class variables

var model_config
var root : str
class PricingModel (*args, **kwds)
Expand source code
class PricingModel(StrEnum):
    cpm = 'cpm'
    vcpm = 'vcpm'
    cpc = 'cpc'
    cpcv = 'cpcv'
    cpv = 'cpv'
    cpp = 'cpp'
    cpa = 'cpa'
    revenue_share = 'revenue_share'
    flat_rate = 'flat_rate'
    time = 'time'

Enum where members are also (and must be) strings

Ancestors

  • enum.StrEnum
  • builtins.str
  • enum.ReprEnum
  • enum.Enum

Class variables

var cpa
var cpc
var cpcv
var cpm
var cpp
var cpv
var flat_rate
var revenue_share
var time
var vcpm
class LegacyProduct (**data: Any)
Expand source code
class Product(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )


    @model_validator(mode='before')
    @classmethod
    def _coerce_publisher_property_models(cls, data: Any) -> Any:
        if isinstance(data, dict) and isinstance(data.get('publisher_properties'), list):
            coerced = []
            changed = False
            for item in data['publisher_properties']:
                if hasattr(item, 'model_dump'):
                    coerced.append(item.model_dump(mode='json', exclude_none=True))
                    changed = True
                else:
                    coerced.append(item)
            if changed:
                data = dict(data)
                data['publisher_properties'] = coerced
        return data
    product_id: Annotated[
        str,
        Field(
            description='Opaque identifier for this buyable product. For a non-custom wholesale product, sellers MUST reuse the ID for the same logical catalog offer within the seller and declared cache_scope across reads and wholesale-feed webhooks; feed and pricing versions communicate temporal catalog mutation, while retirement or replacement may end the identity. Concurrent or request-bound configurations whose effective targeting, disclosed targeting modifications, forecast assumptions, terms, or overlay support differ MUST use distinguishable configured product IDs. For is_custom: true, the ID identifies only the request-specific discovery/refinement lineage and is not stable across independent contexts. Sellers MUST keep every issued configured ID resolvable for its promised lifetime. Pricing variants within one logical product are distinguished by pricing_option_id: a seller MUST mint a new pricing_option_id whenever a binding fixed price, floor, currency, model, or priced applicability changes, and MUST NOT reinterpret an issued option ID at a new price. Selecting product_id plus pricing_option_id in create_media_buy accepts that returned configuration and commercial option.'
        ),
    ]
    name: Annotated[str, Field(description='Human-readable product name')]
    description: Annotated[
        str, Field(description='Detailed description of the product and its inventory')
    ]
    publisher_properties: Annotated[
        list[PublisherProperty],
        Field(
            description="SDK implementers MUST enforce singular-only at runtime: each entry uses the singular `publisher_domain` form; the compact `publisher_domains[]` form is rejected on products. Codegen toolchains (json-schema-to-typescript, quicktype, datamodel-code-generator, openapi-typescript-codegen) often flatten the `allOf + $ref + not.required` restriction below poorly and may drop the rejection constraint silently, emitting an unrestricted type — runtime enforcement is the safety net. Publisher properties covered by this product. Buyers fetch actual property definitions from each publisher's adagents.json and validate agent authorization. Selection patterns mirror the authorization patterns in adagents.json for consistency. The compact `publisher_domains[]` form is reserved for adagents.json `authorized_agents[].publisher_properties[]` so that buy-side traffic-and-pricing flatteners can always treat each entry as exactly one publisher.",
            min_length=1,
        ),
    ]
    channels: Annotated[
        list[channels_1.MediaChannel] | None,
        Field(
            description="Advertising channels this product is sold as. Products inherit from their properties' supported_channels but may narrow the scope. For example, a product covering YouTube properties might be sold as ['ctv'] even though those properties support ['olv', 'social', 'ctv']."
        ),
    ] = None
    format_ids: Annotated[
        list[format_id.FormatReferenceStructuredObject] | None,
        Field(
            deprecated=True,
            description='Deprecated in AdCP 3.2; removed in AdCP 4.0. Legacy named-format compatibility path. Products MUST carry `format_ids`, `format_options`, or both during the 3.x migration window. New products MUST author canonical `format_options[]`; sellers MAY additionally project those declarations to `format_ids` for legacy buyers. When both fields are present they MUST describe the same underlying formats, and buyers MUST prefer `format_options`. Do not author a new product from `format_ids` alone.',
        ),
    ] = None
    format_options: Annotated[
        list[product_format_declaration.ProductFormatDeclaration] | None,
        Field(
            description="Canonical format-option path: one or more inline format declarations the product accepts. Each element narrows a canonical format with parameters, slots, platform_extensions, and optional locale_policy. New 3.2 products MUST carry format_options; a seller MAY additionally project the same declarations to deprecated format_ids for older 3.x peers. A declaration carrying locale_policy is canonical-only because legacy format_ids cannot preserve locale eligibility; no product or placement format_id may project to an effective locale-constrained option.\n\nWhen placements are published, product-level format_options are the union of formats deliverable somewhere in the product and the upper bound for every placement. A placement's effective accepted set is the intersection of every applicable layer: (1) the product format_options; (2) the product placement's inline format_options, when present; and (3) for kind publisher_ref, the named publisher's adagents.json catalog narrowing. Resolve layer 3 by locating the matching placements[] entry: use its format_options when present, resolving bare format_option_id references against that same file's top-level formats[]; otherwise use top-level formats[] applicable to the placement's property_ids/property_tags. An omitted optional layer is unconstrained, but an unresolved publisher placement or format-option reference MUST fail closed. A publisher-referenced placement without inline product format_options therefore does NOT inherit the full product union when the publisher catalog supplies narrower placement or property-scoped acceptance.\n\nMatch publisher-declared options by {publisher_domain, format_option_id}, match product-local options by format_option_id when publisher_domain is omitted, and otherwise match declarations with the same format_kind whose narrower parameters satisfy the broader declaration. A product- or placement-level declaration MUST NOT introduce a format outside the product upper bound. Locale policy follows the same intersection. If the product locale policy is absent, a placement may introduce any concrete policy as a narrowing of an unconstrained option; when both are present, every placement range must be contained by a product range under RFC 4647 Basic Filtering. Locale eligibility is checked independently for every placement where an assignment may serve.\n\nFor a product or package containing multiple included placements, a single creative intended for every placement MUST lie in the intersection of every selected placement's effective set. Distinct per-placement creatives MAY use the union, but the selected creative set MUST cover every included placement; uncovered inventory MUST be rejected or refined, never silently omitted. If a product spans multiple publishers but omits placements[], there is no public routing key for per-placement creatives: its format_options MUST therefore be the common intersection accepted across every selected publisher/property scope. A seller that needs a union of publisher-specific formats MUST publish placements[] with publisher-scoped identities and narrowing. Commercial terms such as price, floor, availability, and deal eligibility are product facts, not format parameters.",
            min_length=1,
        ),
    ] = None
    placements: Annotated[
        list[placement.Placement] | None,
        Field(
            description="Optional array of specific public placements within this product. Placement IDs are scoped by publisher domain. Product placements declare `kind` to distinguish publisher-referenced placements (`publisher_ref`) from seller-defined inline placements (`seller_inline`). Publisher-referenced placements carry `publisher_domain` plus `placement_id` and may omit `name` because buyers resolve the name from the publisher's adagents.json placement declarations. Seller-inline placements carry buyer-facing `name` directly; when `publisher_domain` is omitted, buyers MAY interpret the placement ID relative to the seller agent's own publisher domain only during the legacy single-publisher transition. Community-maintained fallback files are resolver/source metadata, not a distinct placement kind. Each placement MUST declare `mode: 'targetable'` (buyer may purchase it through targeting_overlay.placement_selection) or `mode: 'included'` (part of fixed/default product composition and not independently selectable). Creative assignments route creatives only after placement inventory is purchased. Placement-level format declarations narrow the product-level creative contract and MUST NOT broaden it. Seller-private delivery objects, source/origin details, and ad-server mappings MUST NOT be exposed here.",
            min_length=1,
        ),
    ] = None
    video_placement_types: Annotated[
        list[video_placement_type.VideoPlacementType] | None,
        Field(
            description='Declared video placement types that may be included in this product, using IAB Tech Lab/OpenRTB 2.6 video.plcmt definitions with AdCP-native names. Use on OLV, CTV, and other video products when buyers need to distinguish instream, accompanying-content, interstitial, and standalone/no-content inventory. Aggregate products and ad-network products MAY declare multiple values. When `placements[]` also carry `video_placement_types`, this product-level array SHOULD be the union of the placement-level declarations the seller may deliver under the product. This is seller-declared discovery metadata, not independent verification of inventory quality or delivery context.',
            min_length=1,
        ),
    ] = None
    audio_distribution_types: Annotated[
        list[audio_distribution_type.AudioDistributionType] | None,
        Field(
            description='Declared audio distribution types that may be included in this product, using IAB Tech Lab/OpenRTB 2.6 audio.feed definitions with AdCP-native names. Use on radio, streaming-audio, podcast, gaming, and other audio products when buyers need to distinguish music streaming services, FM/AM broadcast, podcasts, catch-up radio, web radio, video-game audio, and text-to-speech inventory without changing the buyer-facing channel or adagents.json property type. Aggregate products and ad-network products MAY declare multiple values. When `placements[]` also carry `audio_distribution_types`, this product-level array SHOULD be the union of the placement-level declarations the seller may deliver under the product. This is seller-declared discovery metadata, not independent verification of inventory quality or delivery context.',
            min_length=1,
        ),
    ] = None
    sponsored_placement_types: Annotated[
        list[sponsored_placement_type.SponsoredPlacementType] | None,
        Field(
            description='Declared sponsored-placement types that may be included in this product, distinguishing where catalog-driven retail-media placements render on the retailer surface (sponsored search, sponsored display, or sponsored native). Use on retail-media products when buyers need to distinguish search-keyed, display, and native in-grid sponsored inventory. Aggregate products and ad-network products MAY declare multiple values. When `placements[]` also carry `sponsored_placement_types`, this product-level array SHOULD be the union of the placement-level declarations the seller may deliver under the product. This is seller-declared discovery metadata, not independent verification of inventory quality or delivery context.',
            min_length=1,
        ),
    ] = None
    social_placement_surfaces: Annotated[
        list[social_placement_surface.SocialPlacementSurface] | None,
        Field(
            description='Declared social-placement surfaces that may be included in this product, distinguishing the in-app surface where social placements render (feed, stories, short_video, explore, or search). Use on social products when buyers need to distinguish feed, story, short-video, and discovery surfaces. Aggregate products and ad-network products MAY declare multiple values. When `placements[]` also carry `social_placement_surfaces`, this product-level array SHOULD be the union of the placement-level declarations the seller may deliver under the product. This is seller-declared discovery metadata, not independent verification of inventory quality or delivery context.',
            min_length=1,
        ),
    ] = None
    delivery_type: delivery_type_1.DeliveryType
    exclusivity: Annotated[
        exclusivity_1.Exclusivity | None,
        Field(
            description="Whether this product offers exclusive access to its inventory. Defaults to 'none' when absent. Most relevant for guaranteed products tied to specific collections or placements."
        ),
    ] = None
    pricing_options: Annotated[
        list[pricing_option.PricingOption],
        Field(
            description="Available pricing models for this product. Fixed prices and auction floors are binding for every later targeting selection permitted by this product's overlay_support; price_guidance remains non-binding. Declaring broad overlay support alongside a binding option is therefore a uniform-price promise, not permission to calculate a different price at create time. A seller with value-dependent rates MUST return a request-specific configured product after rediscovery with concrete targeting, split the inventory into separately priced products, or expose only non-binding guidance until it can issue a binding option. The seller MUST mint a new pricing_option_id whenever a binding price, floor, currency, model, or priced applicability changes. It MUST NOT silently reprice a create request or reuse the selected option ID with different terms.",
            min_length=1,
        ),
    ]
    forecast: Annotated[
        delivery_forecast.DeliveryForecast | None,
        Field(
            description="Forecasted delivery metrics for this product. Concrete discovery targeting scopes the forecast to those effective values. When discovery requested only required_overlay_support for a dimension, the forecast describes the product's discovery/default scope and is not a value-specific forecast for every later selection; buyers rediscover with concrete targeting_overlay values when they need that forecast."
        ),
    ] = None
    outcome_measurement: Annotated[
        outcome_measurement_1.OutcomeMeasurement | None,
        Field(
            deprecated=True,
            description='**Deprecated as of this minor.** Outcome capabilities (incremental sales lift, brand lift, foot traffic, etc.) are now declared via `reporting_capabilities.available_metrics` (the same path used for impressions, conversions, ROAS) with `qualifier.attribution_methodology` and `qualifier.attribution_window` carrying the methodology and window on commit. New implementations SHOULD use the unified pattern; this field is retained for one-minor backwards compatibility and removed at the next major. See `outcome-measurement.json` description for migration guidance.',
        ),
    ] = None
    delivery_measurement: Annotated[
        DeliveryMeasurement | None,
        Field(
            description='Measurement vendors and methodology for delivery metrics. The buyer accepts the declared vendors as the source of truth for the buy. When absent, buyers should apply their own measurement defaults. Senders SHOULD populate `vendors` (structured BrandRef array) for new implementations; the legacy `provider` string field is deprecated and retained for one-minor backwards compatibility.'
        ),
    ] = None
    measurement_terms: Annotated[
        measurement_terms_1.MeasurementTerms | None,
        Field(
            description="Seller's default billing measurement and makegood terms. Declares who counts the billing metric and what remedies apply when thresholds are breached. Buyers may propose different terms at media buy creation — sellers accept, reject (TERMS_REJECTED), or adjust per their policy."
        ),
    ] = None
    performance_standards: Annotated[
        list[performance_standard.PerformanceStandard] | None,
        Field(
            description="Seller's default performance standards for this product: viewability, IVT, completion rate, brand safety, attention score. Buyers may propose different standards at media buy creation. When absent, no structured performance standards apply.",
            min_length=1,
        ),
    ] = None
    cancellation_policy: Annotated[
        cancellation_policy_1.CancellationPolicy | None,
        Field(
            description='Cancellation terms for this product. Declares the minimum notice period required before cancellation takes effect and any penalties for insufficient notice. Relevant for guaranteed delivery products. Buyers accept these terms by creating a media buy against the product.'
        ),
    ] = None
    allowed_actions: Annotated[
        list[product_allowed_action.ProductAllowedAction] | None,
        Field(
            description='Actions buyers may perform on buys created against this product, scoped to statuses and modes. Advisory template — the authoritative per-buy capability is `available_actions[]` on the buy response, which resolves modes against current buy state, account tier, and negotiated terms. Buyers SHOULD use this for pre-flight product selection ("which products let me self-serve cancel within 72hr?") and read `available_actions[]` for runtime decisions. The array is uniquely keyed by `action` — sellers MUST NOT emit two entries with the same `action` value. Absence means the seller has not declared a structured action surface for this product — buyers fall back to `valid_actions[]` on buy responses for the flat string vocabulary.',
            min_length=1,
        ),
    ] = None
    reporting_capabilities: reporting_capabilities_1.ReportingCapabilities
    creative_policy: creative_policy_1.CreativePolicy | None = None
    is_custom: Annotated[
        bool | None,
        Field(
            description='Whether this product is a request-specific configured offer rather than a reusable baseline product. Sellers MUST set true when targeting, disclosed resolution, pricing, forecast assumptions, inventory, or terms are bound for a particular discovery/refinement lineage. Products issued through targeting-aware discovery include expires_at even when exact acceptance omits targeting_resolution. For backward compatibility, is_custom alone does not make expires_at schema-required.'
        ),
    ] = None
    property_targeting_allowed: Annotated[
        bool | None,
        Field(
            description="Whether buyers can select a subset of this product's publisher_properties through targeting_overlay.property_list. When false, the product is fixed inventory: it matches requested property targeting only when its inherent property set already satisfies the request, or when a configured product discloses additional inventory through targeting_resolution."
        ),
    ] = False
    data_provider_signals: Annotated[
        list[data_provider_signal_selector.DataProviderSignalSelector] | None,
        Field(
            deprecated=True,
            description='Deprecated. Legacy/non-selectable metadata for data-provider signals already bundled into or associated with this product. This field does not provide buyer-selectable options, prices, or seller activation handles. Use included_signals for non-selectable product signal metadata, or signal_targeting_options for selectable package-level signal groups.',
        ),
    ] = None
    included_signals: Annotated[
        list[signal_listing.SignalListing] | None,
        Field(
            description="Non-selectable signal metadata for signals already included in, bundled with, or planned into this product. These signals describe what the product is; buyers do not select them in packages[].targeting_overlay.signal_targeting_groups and this field does not imply package-level signal targeting. Use signal_ref scope 'data_provider' or 'signal_source' to reference externally defined signals without redefining their name or value_type. Use signal_ref scope 'product' with name and value_type when the included signal is defined only by this product.",
            min_length=1,
        ),
    ] = None
    signal_targeting_options: Annotated[
        list[product_signal_targeting_option.ProductSignalTargetingOption] | None,
        Field(
            description="Inline seller-offered signals that may be applied to packages for this product at create_media_buy time. Each entry references a named signal definition with signal_ref scope 'product' for a product-local signal option, scope 'data_provider' for an external signal definition published in adagents.json signals[] that the seller is authorized to apply, or scope 'signal_source' for a source-native signal. Product-local options define name and value_type inline; data-provider and signal-source options may omit those fields when the referenced definition or source is authoritative. Use this field when the selectable menu is product-specific, has product-specific pricing or activation handles, is the relevant subset for a brief/refine result, or should be rendered without an additional get_signals call. Wholesale products may omit this field and rely on get_signals for the selectable signal feed. Buyers select eligible signals through packages[].targeting_overlay.signal_targeting_groups when signal_targeting_rules allow; fixed/default entries are applied by the seller and echoed on the package state. Sellers MUST set signal_targeting_allowed to true whenever this field is present. Bundled, non-selectable signal metadata belongs in included_signals; legacy data_provider_signals may appear only for backwards compatibility.",
            min_length=1,
        ),
    ] = None
    signal_targeting_rules: Annotated[
        signal_targeting_rules_1.SignalTargetingRules | None,
        Field(
            description='Composition rules for selecting signals on this product. The selectable signal menu may come from inline signal_targeting_options or from get_signals when a wholesale product omits inline options. This is product-scoped because products may be backed by different ad servers with different Boolean targeting support and group limits.'
        ),
    ] = None
    signal_targeting_allowed: Annotated[
        bool | None,
        Field(
            description='Whether this product has a package-level signal_targeting_groups surface. When false (default), signals are bundled into the product terms and cannot be selected or explicitly echoed as package signal groups. When true, eligible signals from inline signal_targeting_options or from get_signals may be buyer-selected or seller-applied according to signal_targeting_rules and are represented through packages[].targeting_overlay.signal_targeting_groups. Editability is controlled by signal_targeting_rules; fixed/default-only products still set this to true when applied signal groups are echoed.'
        ),
    ] = False
    demographic_targeting: Annotated[
        demographic_targeting_capability.DemographicTargetingCapability | None,
        Field(
            description='Exact demographic execution available for this product. Buyers MUST use this product-scoped declaration, not the seller-wide get_adcp_capabilities rollup, to preflight a demographic predicate.'
        ),
    ] = None
    overlay_support: Annotated[
        targeting_overlay_support.TargetingOverlaySupport | None,
        Field(
            description='Binding product-scoped targeting dimensions the buyer may set independently on packages after discovery. Presence guarantees selectable capability subject to disclosed limits, not inventory or a forecast for every possible value. Targeting satisfied only through inherent product scope does not appear here. Returned products MUST cover every field requested through get_products.required_overlay_support. A later supported selection with no available inventory returns PRODUCT_UNAVAILABLE on create; an update outside the original priced envelope may return REQUOTE_REQUIRED.'
        ),
    ] = None
    targeting_resolution: Annotated[
        product_targeting_resolution.ProductTargetingResolution | None,
        Field(
            description='Discovery-time targeting resolution bound to this configured product. modifications sparsely disclose product-specific differences from get_products.targeting_overlay. Request-level brief interpretation is returned once on GetProductsResponse.targeting_resolution. Exact structured overlay values are not repeated. Selecting product_id accepts the disclosed modifications; product forecast and pricing MUST reflect them.'
        ),
    ] = None
    audience_evidence: Annotated[
        list[audience_evidence_1.AudienceEvidence] | None,
        Field(
            description='Immutable population-level evidence explaining why this inventory may suit an audience. This supports discovery, comparison, and planning only. It does not imply exact demographic targeting, user-level signal membership, or legal-age verification. Sellers MUST publish each distinct snapshot with a new snapshot_id and content_digest.',
            min_length=1,
        ),
    ] = None
    audience_evidence_selections: Annotated[
        list[audience_evidence_selection.AudienceEvidenceSelection] | None,
        Field(
            description='Exact evidence snapshots that satisfied required eligibility or affected seller ranking for this get_products result. When audience_evidence_requirements was supplied and evidence influenced inclusion or rank, sellers MUST return the relevant selections; an absent-evidence match under evidence_presence when_available has no selection. Product selections use decision_use recommendation or eligibility.',
            min_length=1,
        ),
    ] = None
    catalog_types: Annotated[
        list[catalog_type.CatalogType] | None,
        Field(
            description='Catalog types this product supports for catalog-driven campaigns. A sponsored product listing declares ["product"], a job board declares ["job", "offering"]. Buyers match synced catalogs to products via this field.',
            min_length=1,
        ),
    ] = None
    metric_optimization: Annotated[
        MetricOptimization | None,
        Field(
            description="Metric optimization capabilities for this product. Presence indicates the product supports optimization_goals with kind: 'metric'. No event source or conversion tracking setup required — the seller tracks these metrics natively."
        ),
    ] = None
    vendor_metric_optimization: Annotated[
        vendor_metric_optimization_1.VendorMetricOptimization | None,
        Field(
            description="Vendor-attested metric optimization capabilities for this product. Presence indicates the product supports `optimization_goals` with `kind: 'vendor_metric'` — the seller's bidding stack can steer delivery toward a specific vendor's measurement (e.g., DV/IAS/Adelaide attention, Scope3 emissions, Kantar brand lift, retail-media partner metrics). Distinct from `metric_optimization` (seller-native metrics with no vendor binding) and from `reporting_capabilities.vendor_metrics` (which declares what the product can *report* rather than what it can *optimize against*). A product may report a vendor metric without being able to optimize for it. Buyers MUST verify the goal's `(vendor, metric_id)` is in `supported_metrics` AND that the package's `committed_metrics[]` includes a matching `{ scope: 'vendor', vendor, metric_id }` entry — optimization without committed reporting is unverifiable and is rejected at the wire level."
        ),
    ] = None
    max_optimization_goals: Annotated[
        int | None,
        Field(
            description='Maximum number of optimization_goals this product accepts on a package. When absent, no limit is declared. Most social platforms accept only 1 goal — buyers sending arrays longer than this value should expect the seller to use only the highest-priority (lowest priority number) goal.',
            ge=1,
        ),
    ] = None
    measurement_readiness: Annotated[
        measurement_readiness_1.MeasurementReadiness | None,
        Field(
            description="Assessment of whether the buyer's event source setup is sufficient for this product to optimize effectively. Only present when the seller can evaluate the buyer's account context. Buyers should check this before creating media buys with event-based optimization goals."
        ),
    ] = None
    conversion_tracking: Annotated[
        ConversionTracking | None,
        Field(
            description="Conversion event tracking for this product. Presence indicates the product supports optimization_goals with kind: 'event'. Seller-level capabilities (supported event types, UID types, attribution windows) are declared in get_adcp_capabilities."
        ),
    ] = None
    catalog_match: Annotated[
        CatalogMatch | None,
        Field(
            description='When the buyer provides a catalog on get_products, indicates which catalog items are eligible for this product. Only present for products where catalog matching is relevant (e.g., sponsored product listings, job boards, hotel ads).'
        ),
    ] = None
    brief_relevance: Annotated[
        str | None,
        Field(
            description='Explanation of why this product matches the brief (only included when brief is provided)'
        ),
    ] = None
    expires_at: Annotated[
        AwareDatetime | None,
        Field(
            description='Expiration timestamp. Required for request-specific configured products whose targeting resolution, price, forecast, inventory, or terms are time-bound. After this time, a seller that still recognizes the issued configured ID within the authenticated account and referenced discovery/refinement lineage rejects create_media_buy with PRODUCT_EXPIRED and the buyer re-runs get_products. Once the seller no longer retains an expiry tombstone, or whenever the ID belongs to another account or lineage, PRODUCT_NOT_FOUND applies instead; sellers are not required to retain tombstones indefinitely and MUST NOT disclose cross-tenant existence through error choice.'
        ),
    ] = None
    product_card: Annotated[
        ProductCard | None,
        Field(
            description='Optional standard visual card for displaying this product in user interfaces (catalog browsers, dashboards, agent UIs). Distinct from `format` — product_card describes the UI rendering of the product itself, not the ad creative the product accepts. Typed inline; no format_id indirection. Receivers render the card directly from these fields.'
        ),
    ] = None
    product_card_detailed: Annotated[
        ProductCardDetailed | None,
        Field(
            description='Optional detailed card with hero + carousel + structured specifications, for rich product presentation (media-kit-style pages, full product detail views). Distinct from `format` — describes the UI rendering of the product itself, not the ad creative the product accepts. Typed inline; no format_id indirection.'
        ),
    ] = None
    collections: Annotated[
        list[collection_selector.CollectionSelector] | None,
        Field(
            description='Collections available in this product. Each entry references collections declared in an adagents.json by domain and collection ID. Buyers resolve full collection objects from the referenced adagents.json.',
            min_length=1,
        ),
    ] = None
    collection_targeting_allowed: Annotated[
        bool | None,
        Field(
            description="Whether buyers can select a subset of this product's collections through targeting_overlay.collection_list. When false, the product is a fixed bundle; when true, collection selection is a product-scoped overlay capability."
        ),
    ] = False
    installments: Annotated[
        list[installment.Installment] | None,
        Field(
            description='Specific installments included in this product. Each installment references its parent collection via collection_id when the product spans multiple collections. When absent with collections present, the product covers the collections broadly (run-of-collection).'
        ),
    ] = None
    enforced_policies: Annotated[
        list[str] | None,
        Field(
            description='Registry policy IDs the seller enforces for this product. Enforcement level comes from the policy registry. Buyers can filter products by required policies.'
        ),
    ] = None
    trusted_match: Annotated[
        TrustedMatch | None,
        Field(
            description='Trusted Match Protocol capabilities for this product. When present, the product supports real-time contextual and/or identity matching via TMP. Buyers use this to determine what response types the publisher can accept and whether brands can be selected dynamically at match time.'
        ),
    ] = None
    material_submission: Annotated[
        MaterialSubmission | None,
        Field(
            description="Instructions for submitting physical creative materials (print, static OOH, cinema). Present only for products requiring physical delivery outside the digital creative assignment flow. Buyer agents MUST validate url and email domains against the seller's known domains (from adagents.json) before submitting materials. Never auto-submit without human confirmation."
        ),
    ] = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var allowed_actions : list[adcp.types.generated_poc.core.product_allowed_action.ProductAllowedAction] | None
var audience_evidence : list[adcp.types.generated_poc.core.audience_evidence.AudienceEvidence] | None
var audience_evidence_selections : list[adcp.types.generated_poc.core.audience_evidence_selection.AudienceEvidenceSelection] | None
var audio_distribution_types : list[adcp.types.generated_poc.enums.audio_distribution_type.AudioDistributionType] | None
var brief_relevance : str | None
var cancellation_policy : adcp.types.generated_poc.core.cancellation_policy.CancellationPolicy | None
var catalog_match : adcp.types.generated_poc.core.product.CatalogMatch | None
var catalog_types : list[adcp.types.generated_poc.enums.catalog_type.CatalogType] | None
var channels : list[adcp.types.generated_poc.enums.channels.MediaChannel] | None
var collection_targeting_allowed : bool | None
var collections : list[adcp.types.generated_poc.core.collection_selector.CollectionSelector] | None
var conversion_tracking : adcp.types.generated_poc.core.product.ConversionTracking | None
var creative_policy : adcp.types.generated_poc.core.creative_policy.CreativePolicy | None
var data_provider_signals : list[adcp.types.generated_poc.core.data_provider_signal_selector.DataProviderSignalSelector] | None
var delivery_measurement : adcp.types.generated_poc.core.product.DeliveryMeasurement | None
var delivery_type : adcp.types.generated_poc.enums.delivery_type.DeliveryType
var demographic_targeting : adcp.types.generated_poc.core.demographic_targeting_capability.DemographicTargetingCapability | None
var description : str
var enforced_policies : list[str] | None
var exclusivity : adcp.types.generated_poc.enums.exclusivity.Exclusivity | None
var expires_at : pydantic.types.AwareDatetime | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var forecast : adcp.types.generated_poc.core.delivery_forecast.DeliveryForecast | None
var format_ids : list[adcp.types.generated_poc.core.format_id.FormatReferenceStructuredObject] | None
var format_options : list[adcp.types.generated_poc.core.product_format_declaration.ProductFormatDeclaration] | None
var included_signals : list[adcp.types.generated_poc.core.signal_listing.SignalListing] | None
var installments : list[adcp.types.generated_poc.core.installment.Installment] | None
var is_custom : bool | None
var material_submission : adcp.types.generated_poc.core.product.MaterialSubmission | None
var max_optimization_goals : int | None
var measurement_readiness : adcp.types.generated_poc.core.measurement_readiness.MeasurementReadiness | None
var measurement_terms : adcp.types.generated_poc.core.measurement_terms.MeasurementTerms | None
var metric_optimization : adcp.types.generated_poc.core.product.MetricOptimization | None
var model_config
var name : str
var outcome_measurement : adcp.types.generated_poc.core.outcome_measurement.OutcomeMeasurement | None
var overlay_support : adcp.types.generated_poc.core.targeting_overlay_support.TargetingOverlaySupport | None
var performance_standards : list[adcp.types.generated_poc.core.performance_standard.PerformanceStandard] | None
var placements : list[adcp.types.generated_poc.core.placement.Placement] | None
var pricing_options : list[adcp.types.generated_poc.core.pricing_option.PricingOption]
var product_card : adcp.types.generated_poc.core.product.ProductCard | None
var product_card_detailed : adcp.types.generated_poc.core.product.ProductCardDetailed | None
var product_id : str
var property_targeting_allowed : bool | None
var publisher_properties : list[adcp.types.generated_poc.core.product.PublisherProperty]
var reporting_capabilities : adcp.types.generated_poc.core.reporting_capabilities.ReportingCapabilities
var signal_targeting_allowed : bool | None
var signal_targeting_options : list[adcp.types.generated_poc.core.product_signal_targeting_option.ProductSignalTargetingOption] | None
var signal_targeting_rules : adcp.types.generated_poc.core.signal_targeting_rules.SignalTargetingRules | None
var social_placement_surfaces : list[adcp.types.generated_poc.enums.social_placement_surface.SocialPlacementSurface] | None
var sponsored_placement_types : list[adcp.types.generated_poc.enums.sponsored_placement_type.SponsoredPlacementType] | None
var targeting_resolution : adcp.types.generated_poc.core.product_targeting_resolution.ProductTargetingResolution | None
var trusted_match : adcp.types.generated_poc.core.product.TrustedMatch | None
var vendor_metric_optimization : adcp.types.generated_poc.core.vendor_metric_optimization.VendorMetricOptimization | None
var video_placement_types : list[adcp.types.generated_poc.enums.video_placement_type.VideoPlacementType] | None
class Product (**data: Any)

Base class enforcing the primary canonical runtime boundary.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var allowed_actions : list[adcp.types.generated_poc.core.product_allowed_action.ProductAllowedAction] | None
var audience_evidence : list[adcp.types.generated_poc.core.audience_evidence.AudienceEvidence] | None
var audience_evidence_selections : list[adcp.types.generated_poc.core.audience_evidence_selection.AudienceEvidenceSelection] | None
var audio_distribution_types : list[adcp.types.generated_poc.enums.audio_distribution_type.AudioDistributionType] | None
var brief_relevance : str | None
var cancellation_policy : adcp.types.generated_poc.core.cancellation_policy.CancellationPolicy | None
var catalog_match : adcp.types.generated_poc.core.product.CatalogMatch | None
var catalog_types : list[adcp.types.generated_poc.enums.catalog_type.CatalogType] | None
var channels : list[adcp.types.generated_poc.enums.channels.MediaChannel] | None
var collection_targeting_allowed : bool | None
var collections : list[adcp.types.generated_poc.core.collection_selector.CollectionSelector] | None
var conversion_tracking : adcp.types.generated_poc.core.product.ConversionTracking | None
var creative_policy : adcp.types.generated_poc.core.creative_policy.CreativePolicy | None
var data_provider_signals : list[adcp.types.generated_poc.core.data_provider_signal_selector.DataProviderSignalSelector] | None
var delivery_measurement : adcp.types.generated_poc.core.product.DeliveryMeasurement | None
var delivery_type : adcp.types.generated_poc.enums.delivery_type.DeliveryType
var demographic_targeting : adcp.types.generated_poc.core.demographic_targeting_capability.DemographicTargetingCapability | None
var description : str
var enforced_policies : list[str] | None
var exclusivity : adcp.types.generated_poc.enums.exclusivity.Exclusivity | None
var expires_at : pydantic.types.AwareDatetime | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var forecast : adcp.types.generated_poc.core.delivery_forecast.DeliveryForecast | None
var format_options : list[Format]
var included_signals : list[adcp.types.generated_poc.core.signal_listing.SignalListing] | None
var installments : list[adcp.types.generated_poc.core.installment.Installment] | None
var is_custom : bool | None
var material_submission : adcp.types.generated_poc.core.product.MaterialSubmission | None
var max_optimization_goals : int | None
var measurement_readiness : adcp.types.generated_poc.core.measurement_readiness.MeasurementReadiness | None
var measurement_terms : adcp.types.generated_poc.core.measurement_terms.MeasurementTerms | None
var metric_optimization : adcp.types.generated_poc.core.product.MetricOptimization | None
var model_config
var name : str
var outcome_measurement : adcp.types.generated_poc.core.outcome_measurement.OutcomeMeasurement | None
var overlay_support : adcp.types.generated_poc.core.targeting_overlay_support.TargetingOverlaySupport | None
var performance_standards : list[adcp.types.generated_poc.core.performance_standard.PerformanceStandard] | None
var placements : list[Placement] | None
var pricing_options : list[adcp.types.generated_poc.core.pricing_option.PricingOption]
var product_card : adcp.types.generated_poc.core.product.ProductCard | None
var product_card_detailed : adcp.types.generated_poc.core.product.ProductCardDetailed | None
var product_id : str
var property_targeting_allowed : bool | None
var publisher_properties : list[adcp.types.generated_poc.core.product.PublisherProperty]
var reporting_capabilities : adcp.types.generated_poc.core.reporting_capabilities.ReportingCapabilities
var signal_targeting_allowed : bool | None
var signal_targeting_options : list[adcp.types.generated_poc.core.product_signal_targeting_option.ProductSignalTargetingOption] | None
var signal_targeting_rules : adcp.types.generated_poc.core.signal_targeting_rules.SignalTargetingRules | None
var social_placement_surfaces : list[adcp.types.generated_poc.enums.social_placement_surface.SocialPlacementSurface] | None
var sponsored_placement_types : list[adcp.types.generated_poc.enums.sponsored_placement_type.SponsoredPlacementType] | None
var targeting_resolution : adcp.types.generated_poc.core.product_targeting_resolution.ProductTargetingResolution | None
var trusted_match : adcp.types.generated_poc.core.product.TrustedMatch | None
var vendor_metric_optimization : adcp.types.generated_poc.core.vendor_metric_optimization.VendorMetricOptimization | None
var video_placement_types : list[adcp.types.generated_poc.enums.video_placement_type.VideoPlacementType] | None

Inherited members

class LegacyProductFilters (**data: Any)
Expand source code
class ProductFilters(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    delivery_type: delivery_type_1.DeliveryType | None = None
    exclusivity: Annotated[
        exclusivity_1.Exclusivity | None,
        Field(
            description="Filter by exclusivity level. Returns products matching the specified exclusivity (e.g., 'exclusive' returns only sole-sponsorship products)."
        ),
    ] = None
    is_fixed_price: Annotated[
        bool | None,
        Field(
            description='Legacy filter for fixed versus auction pricing availability. true returns options with fixed_price; false returns auction options whose price is established through bid_price. Contingent options such as revenue_share match neither value and MUST be omitted whenever this filter is present. Use pricing_structures to discover contingent pricing. Products with both fixed and auction options match both true and false, but sellers MUST return only entries matching the requested structure.'
        ),
    ] = None
    pricing_structures: Annotated[
        list[pricing_structure.PricingStructure] | None,
        Field(
            description='Filter by how the payable price is determined. fixed selects options with fixed_price, auction selects options established through bid_price, and contingent selects options calculated from a measured business outcome after delivery (currently revenue_share). Products match when at least one pricing option has a requested structure. Sellers MUST return only matching pricing_options entries. When combined with is_fixed_price, both filters apply and the returned entries must satisfy both.',
            min_length=1,
        ),
    ] = None
    pricing_currencies: Annotated[
        list[PricingCurrency] | None,
        Field(
            description='Filter by currencies the buyer can use for the media product transaction, using ISO 4217 currency codes. Products match when they offer at least one product-level pricing_options entry in one of the requested currencies and any seller-applied or otherwise mandatory product-scoped signal charges are satisfiable in one of those currencies or have no incremental price. Mandatory custom signal pricing without currency is not satisfiable for this filter unless the seller can truthfully treat it as having no incremental price. Sellers MUST return only product pricing_options entries whose currency is in this list so buyers can select deterministically from discovery. This filter does not require pruning optional signal or vendor add-on pricing; buyers should avoid optional add-ons priced only in unsupported currencies.',
            min_length=1,
        ),
    ] = None
    format_ids: Annotated[
        list[format_id.FormatReferenceStructuredObject] | None,
        Field(
            deprecated=True,
            description='Deprecated in AdCP 3.2; removed in AdCP 4.0. Filter by legacy named-format references. Use `format_kinds` or `format_option_refs`.',
            min_length=1,
        ),
    ] = None
    format_kinds: Annotated[
        list[canonical_format_kind.CanonicalFormatKind] | None,
        Field(
            description='Filter to products accepting any of these canonical format kinds.',
            min_length=1,
        ),
    ] = None
    format_option_refs: Annotated[
        list[format_option_ref.FormatOptionReference] | None,
        Field(
            description='Filter to products accepting any of these exact publisher- or product-scoped canonical format options.',
            min_length=1,
        ),
    ] = None
    standard_formats_only: Annotated[
        bool | None, Field(description='Only return products accepting IAB standard formats')
    ] = None
    min_exposures: Annotated[
        int | None,
        Field(description='Minimum exposures/impressions needed for measurement validity', ge=1),
    ] = None
    start_date: Annotated[
        date | None,
        Field(
            description='Campaign start date (ISO 8601 date format: YYYY-MM-DD) for availability checks'
        ),
    ] = None
    end_date: Annotated[
        date | None,
        Field(
            description='Campaign end date (ISO 8601 date format: YYYY-MM-DD) for availability checks'
        ),
    ] = None
    budget_range: Annotated[
        BudgetRange | None, Field(description='Budget range to filter appropriate products')
    ] = None
    countries: Annotated[
        list[Country] | None,
        Field(
            deprecated=True,
            description='DEPRECATED. Use get_products.targeting_overlay.geo_countries so returned pricing and forecasts are scoped to the concrete delivery constraint.',
            min_length=1,
        ),
    ] = None
    regions: Annotated[
        list[Region] | None,
        Field(
            deprecated=True,
            description='DEPRECATED. Use get_products.targeting_overlay.geo_regions. Sellers resolve the requested outcome through inherent product scope or selectable targeting.',
            min_length=1,
        ),
    ] = None
    metros: Annotated[
        list[Metro] | None,
        Field(
            deprecated=True,
            description='DEPRECATED. Use get_products.targeting_overlay.geo_metros for known values or required_overlay_support.geo_metros when values will be supplied on packages later.',
            min_length=1,
        ),
    ] = None
    channels: Annotated[
        list[channels_1.MediaChannel] | None,
        Field(
            description="Filter by advertising channels (e.g., ['display', 'ctv', 'dooh'])",
            min_length=1,
        ),
    ] = None
    video_placement_types: Annotated[
        list[video_placement_type.VideoPlacementType] | None,
        Field(
            description='Filter product metadata by declared video placement types, using IAB Tech Lab/OpenRTB 2.6 video.plcmt definitions with AdCP-native names. A product matches when its declared array intersects the requested array. This is discovery classification only and does not promise delivery exclusively on a requested type; buyers needing exact placement inventory use targeting_overlay.placement_selection against targetable placements. This filter has set semantics for wholesale feed canonicalization.',
            min_length=1,
        ),
    ] = None
    audio_distribution_types: Annotated[
        list[audio_distribution_type.AudioDistributionType] | None,
        Field(
            description='Filter product metadata by declared audio distribution types, using IAB Tech Lab/OpenRTB 2.6 audio.feed definitions with AdCP-native names. A product matches when its declared array intersects the requested array. This is discovery classification only and does not promise delivery exclusively on a requested type. This filter has set semantics for wholesale feed canonicalization.',
            min_length=1,
        ),
    ] = None
    sponsored_placement_types: Annotated[
        list[sponsored_placement_type.SponsoredPlacementType] | None,
        Field(
            description='Filter retail-media product metadata by declared sponsored-placement types (sponsored search, sponsored display, or sponsored native). A product matches when its declared array intersects the requested array. This is discovery classification only and does not promise delivery exclusively on a requested type. This filter has set semantics for wholesale feed canonicalization.',
            min_length=1,
        ),
    ] = None
    social_placement_surfaces: Annotated[
        list[social_placement_surface.SocialPlacementSurface] | None,
        Field(
            description='Filter social-product metadata by declared placement surfaces (feed, stories, short_video, explore, or search). A product matches when its declared array intersects the requested array. This is discovery classification only and does not promise delivery exclusively on a requested surface; buyers needing an exact public placement use targeting_overlay.placement_selection. This filter has set semantics for wholesale feed canonicalization.',
            min_length=1,
        ),
    ] = None
    required_axe_integrations: Annotated[
        list[AnyUrl] | None,
        Field(
            deprecated=True,
            description='Deprecated: Use trusted_match filter instead. Filter to products executable through specific agentic ad exchanges. URLs are canonical identifiers.',
        ),
    ] = None
    trusted_match: Annotated[
        TrustedMatch | None,
        Field(
            description='Filter products by Trusted Match Protocol capabilities. Only products with matching TMP support are returned.'
        ),
    ] = None
    required_features: Annotated[
        media_buy_features.MediaBuyFeatures | None,
        Field(
            description='Filter to products from sellers supporting specific protocol features. Only features set to true are used for filtering.'
        ),
    ] = None
    required_geo_targeting: Annotated[
        list[RequiredGeoTargetingItem] | None,
        Field(
            deprecated=True,
            description='DEPRECATED. Use get_products.required_overlay_support, which is product-scoped and applies consistently to geographic and non-geographic targeting dimensions.',
            min_length=1,
        ),
    ] = None
    signal_targeting: Annotated[
        list[SignalTargetingItem] | None,
        Field(
            deprecated=True,
            description='DEPRECATED. Use get_products.targeting_overlay.signal_targeting_groups for known selections or required_overlay_support.signal_targeting_groups when selection will happen later. Legacy entries remain accepted during migration.',
            min_length=1,
        ),
    ] = None
    postal_areas: Annotated[
        list[postal_area.PostalArea] | None,
        Field(
            deprecated=True,
            description='DEPRECATED. Use get_products.targeting_overlay.geo_postal_areas for known values or required_overlay_support.geo_postal_areas when values will be supplied later.',
            min_length=1,
        ),
    ] = None
    geo_proximity: Annotated[
        list[GeoProximityItem] | None,
        Field(
            deprecated=True,
            description='DEPRECATED. Use get_products.targeting_overlay.geo_proximity. Sellers resolve the requested outcome through inherent product scope or selectable targeting and forecast the resulting inventory.',
            min_length=1,
        ),
    ] = None
    required_performance_standards: Annotated[
        list[performance_standard.PerformanceStandard] | None,
        Field(
            description="Filter to products that can meet the buyer's performance standard requirements. Each entry specifies a metric, minimum threshold, and optionally a required vendor and standard. Products that cannot meet these thresholds or do not support the specified vendors are excluded. Use this to tell the seller upfront: 'I need DoubleVerify for viewability at 70% MRC.'",
            min_length=1,
        ),
    ] = None
    required_metrics: Annotated[
        list[available_metric.AvailableMetric] | None,
        Field(
            description="Filter to products whose `reporting_capabilities.available_metrics` is a superset of these metrics — i.e., products that commit to reporting all listed metrics in delivery responses. Use this for capability-level discovery (e.g., 'I need products that report `completed_views` for a CTV CPCV buy'); guarantee-level requirements with thresholds belong in `required_performance_standards` and `measurement_terms`. Sellers MUST silently exclude products that cannot meet this list (filter-not-fail; do not return an error). The product's declared `available_metrics` becomes the binding reporting contract carried into the resulting media buy — the same metric vocabulary is used to compute `missing_metrics` on `get_media_buy_delivery`.",
            examples=[
                ['completed_views'],
                ['completed_views', 'completion_rate'],
                ['impressions', 'spend', 'engagements'],
            ],
            min_length=1,
        ),
    ] = None
    required_vendor_metrics: Annotated[
        list[RequiredVendorMetric] | None,
        Field(
            description="Filter to products whose `reporting_capabilities.vendor_metrics` matches these criteria. Each entry pins a `vendor` (matches any metric from that vendor), a `metric_id` (matches the metric across any vendor that uses that identifier), or both (specific vendor's specific metric). A product matches if its declared `vendor_metrics` covers ALL listed entries (AND across entries; pins within an entry are conjunctive). Cross-vendor discovery (e.g., 'I need attention measurement from any vendor that does it') is the buyer agent's responsibility — the agent resolves which vendors offer a category via the vendors' `brand.json` records, then enumerates them as filter entries. AdCP does not carry vendor-side metric metadata (category, methodology, standard alignment) in the filter surface; that lives at the vendor and is queried out-of-band. Sellers MUST silently exclude non-matching products (filter-not-fail; do not return an error) — same convention as the other `required_*` filters.",
            examples=[
                [{'vendor': {'domain': 'attentionvendor.example'}}],
                [
                    {
                        'vendor': {'domain': 'panelmeasurement.example'},
                        'metric_id': 'demographic_reach',
                    }
                ],
                [
                    {'vendor': {'domain': 'attentionvendor.example'}},
                    {'vendor': {'domain': 'secondattentionvendor.example'}},
                ],
            ],
            min_length=1,
        ),
    ] = None
    keywords: Annotated[
        list[Keyword] | None,
        Field(
            deprecated=True,
            description='DEPRECATED. Use get_products.targeting_overlay.keyword_targets for concrete terms or required_overlay_support.keyword_targets when terms will be supplied later. Broad thematic intent remains in brief.',
            min_length=1,
        ),
    ] = None
    audience_evidence_requirements: Annotated[
        audience_evidence_requirements_1.AudienceEvidenceRequirements | None,
        Field(
            description='Buyer policy for evaluating Product.audience_evidence. In required mode, sellers MUST apply the evidence_presence and admissibility semantics and exclude non-matching products; they MUST NOT ignore an unsupported hard requirement. In preferred mode, sellers use matches for ranking and explain the evidence selected. Buyers SHOULD inspect media_buy.audience_evidence capabilities before sending this object.'
        ),
    ] = None
    ext: Annotated[
        ext_1.ExtensionObject | None,
        Field(
            description='Vendor-namespaced extension parameters for seller-specific filter criteria not covered by standard fields. Keys MUST be namespaced under a vendor or platform key (e.g., ext.gam, ext.platform_x). Sellers MUST treat all values as untrusted buyer input; do not interpolate into LLM prompts, SQL queries, or system commands without sanitization. Persistent use of an extension key across multiple buyers is a signal to propose standardization.'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var audience_evidence_requirements : adcp.types.generated_poc.core.audience_evidence_requirements.AudienceEvidenceRequirements | None
var audio_distribution_types : list[adcp.types.generated_poc.enums.audio_distribution_type.AudioDistributionType] | None
var budget_range : adcp.types.generated_poc.core.product_filters.BudgetRange | None
var channels : list[adcp.types.generated_poc.enums.channels.MediaChannel] | None
var countries : list[adcp.types.generated_poc.core.product_filters.Country] | None
var delivery_type : adcp.types.generated_poc.enums.delivery_type.DeliveryType | None
var end_date : datetime.date | None
var exclusivity : adcp.types.generated_poc.enums.exclusivity.Exclusivity | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var format_ids : list[adcp.types.generated_poc.core.format_id.FormatReferenceStructuredObject] | None
var format_kinds : list[adcp.types.generated_poc.core.canonical_format_kind.CanonicalFormatKind] | None
var format_option_refs : list[adcp.types.generated_poc.core.format_option_ref.FormatOptionReference] | None
var geo_proximity : list[adcp.types.generated_poc.core.product_filters.GeoProximityItem] | None
var is_fixed_price : bool | None
var keywords : list[adcp.types.generated_poc.core.product_filters.Keyword] | None
var metros : list[adcp.types.generated_poc.core.product_filters.Metro] | None
var min_exposures : int | None
var model_config
var postal_areas : list[adcp.types.generated_poc.core.postal_area.PostalArea] | None
var pricing_currencies : list[adcp.types.generated_poc.core.product_filters.PricingCurrency] | None
var pricing_structures : list[adcp.types.generated_poc.enums.pricing_structure.PricingStructure] | None
var regions : list[adcp.types.generated_poc.core.product_filters.Region] | None
var required_axe_integrations : list[pydantic.networks.AnyUrl] | None
var required_features : adcp.types.generated_poc.core.media_buy_features.MediaBuyFeatures | None
var required_geo_targeting : list[adcp.types.generated_poc.core.product_filters.RequiredGeoTargetingItem] | None
var required_metrics : list[adcp.types.generated_poc.enums.available_metric.AvailableMetric] | None
var required_performance_standards : list[adcp.types.generated_poc.core.performance_standard.PerformanceStandard] | None
var required_vendor_metrics : list[adcp.types.generated_poc.core.product_filters.RequiredVendorMetric] | None
var signal_targeting : list[adcp.types.generated_poc.core.product_filters.SignalTargetingItem] | None
var social_placement_surfaces : list[adcp.types.generated_poc.enums.social_placement_surface.SocialPlacementSurface] | None
var sponsored_placement_types : list[adcp.types.generated_poc.enums.sponsored_placement_type.SponsoredPlacementType] | None
var standard_formats_only : bool | None
var start_date : datetime.date | None
var trusted_match : adcp.types.generated_poc.core.product_filters.TrustedMatch | None
var video_placement_types : list[adcp.types.generated_poc.enums.video_placement_type.VideoPlacementType] | None
class ProductFilters (**data: Any)

Base class enforcing the primary canonical runtime boundary.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var audience_evidence_requirements : adcp.types.generated_poc.core.audience_evidence_requirements.AudienceEvidenceRequirements | None
var audio_distribution_types : list[adcp.types.generated_poc.enums.audio_distribution_type.AudioDistributionType] | None
var budget_range : adcp.types.generated_poc.core.product_filters.BudgetRange | None
var channels : list[adcp.types.generated_poc.enums.channels.MediaChannel] | None
var countries : list[adcp.types.generated_poc.core.product_filters.Country] | None
var delivery_type : adcp.types.generated_poc.enums.delivery_type.DeliveryType | None
var end_date : datetime.date | None
var exclusivity : adcp.types.generated_poc.enums.exclusivity.Exclusivity | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var format_kinds : list[adcp.types.generated_poc.core.canonical_format_kind.CanonicalFormatKind] | None
var format_option_refs : list[adcp.types.generated_poc.core.format_option_ref.FormatOptionReference] | None
var geo_proximity : list[adcp.types.generated_poc.core.product_filters.GeoProximityItem] | None
var is_fixed_price : bool | None
var keywords : list[adcp.types.generated_poc.core.product_filters.Keyword] | None
var metros : list[adcp.types.generated_poc.core.product_filters.Metro] | None
var min_exposures : int | None
var model_config
var postal_areas : list[adcp.types.generated_poc.core.postal_area.PostalArea] | None
var pricing_currencies : list[adcp.types.generated_poc.core.product_filters.PricingCurrency] | None
var pricing_structures : list[adcp.types.generated_poc.enums.pricing_structure.PricingStructure] | None
var regions : list[adcp.types.generated_poc.core.product_filters.Region] | None
var required_axe_integrations : list[pydantic.networks.AnyUrl] | None
var required_features : adcp.types.generated_poc.core.media_buy_features.MediaBuyFeatures | None
var required_geo_targeting : list[adcp.types.generated_poc.core.product_filters.RequiredGeoTargetingItem] | None
var required_metrics : list[adcp.types.generated_poc.enums.available_metric.AvailableMetric] | None
var required_performance_standards : list[adcp.types.generated_poc.core.performance_standard.PerformanceStandard] | None
var required_vendor_metrics : list[adcp.types.generated_poc.core.product_filters.RequiredVendorMetric] | None
var signal_targeting : list[adcp.types.generated_poc.core.product_filters.SignalTargetingItem] | None
var social_placement_surfaces : list[adcp.types.generated_poc.enums.social_placement_surface.SocialPlacementSurface] | None
var sponsored_placement_types : list[adcp.types.generated_poc.enums.sponsored_placement_type.SponsoredPlacementType] | None
var standard_formats_only : bool | None
var start_date : datetime.date | None
var trusted_match : adcp.types.generated_poc.core.product_filters.TrustedMatch | None
var video_placement_types : list[adcp.types.generated_poc.enums.video_placement_type.VideoPlacementType] | None

Inherited members

class LegacyProductFormatDeclaration (**data: Any)
Expand source code
class ProductFormatDeclaration(AdCPBaseModel):
    format_option_id: Annotated[
        str | None,
        Field(
            description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'."
        ),
    ] = None
    publisher_domain: Annotated[
        str | None,
        Field(
            description="Namespace for `format_option_id` when this declaration references or narrows a publisher-declared format option from that publisher's adagents.json top-level `formats[]`. Product-local options omit this field and are selected by `format_option_id` within the target product.",
            pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$',
        ),
    ] = None
    display_name: Annotated[
        str | None,
        Field(
            description="Optional seller-controlled human-readable label for this format declaration. Used by buyer dashboards, catalog UIs, and reporting surfaces to show a seller's own naming ('Homepage Takeover', 'Branded Canvas', 'Reels Premium Video') rather than the raw `format_kind` or `format_option_id`. Has no machine semantics — buyer agents route on `format_kind` and `format_option_id`; `display_name` is purely for human presentation. Freeform; no enumeration. Sellers SHOULD keep it stable once published to avoid dashboard churn."
        ),
    ] = None
    sample_render_url: Annotated[
        AnyUrl | None,
        Field(
            description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.'
        ),
    ] = None
    applies_to_channels: Annotated[
        list[channels.MediaChannel] | None,
        Field(
            description="Optional subset of the parent product's `channels` to which this declaration applies. When omitted, the declaration applies to ALL channels declared on the product. Lets a multi-channel product (e.g., `channels: ['display', 'video']`) carry distinct format_options per channel — `format_options: [{format_kind: 'image', applies_to_channels: ['display']}, {format_kind: 'video_hosted', applies_to_channels: ['video']}]`. Buyers ship channel-appropriate manifests per `applies_to_channels`."
        ),
    ] = None
    seller_preference: Annotated[
        SellerPreference | None,
        Field(
            description="Optional soft routing hint *within* a product's accepted set of formats — NOT an enforcement axis. `preferred` — seller actively recommends this format (often because of measurement, viewability, or render-quality differences); `accepted` — supported on equal footing with other format_options (default when omitted); `discouraged` — supported but suboptimal (e.g., legacy 3p-tag where the seller would prefer html5 for OM-SDK coverage). Buyer agents picking between format_options SHOULD respect seller preferences when their own constraints don't override.\n\n**Not an enforcement axis (normative).** `seller_preference` does NOT carry the meaning of 'this format won't work / required-only'. That case is structural: `format_options[]` IS the closed set of accepted formats; anything outside the list is rejected at `create_media_buy` regardless of preference. A seller that accepts only one format lists exactly that one entry — the structural fact does the enforcement work, no enum value needed. There is intentionally no `required` value; preference is bounded to *ranking within the already-accepted set*, not gating into it."
        ),
    ] = None
    locale_policy: Annotated[
        creative_locale_policy.CreativeLocalePolicy | None,
        Field(
            description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.'
        ),
    ] = None
    canonical_formats_only: Annotated[
        bool | None,
        Field(
            description='When true, this format declaration has no clean v1 projection and SDKs MUST NOT synthesize a v1 `format_id` for it. Buyers reading the product on the v1 wire path see this declaration absent from `format_ids`; only v2-aware buyers (reading `format_options`) discover it. Set explicitly for `format_kind: "custom"` declarations (no canonical exists in v1 to project onto) and for declarations whose canonical/parameter shape cannot round-trip through a v1 named format without semantic loss. The protocol does NOT mint synthetic v1 format_ids for unmappable declarations — the alternative (an `aao-synth/*` namespace populated automatically) was considered and rejected because adopters would index on synthetic IDs that have no stable identity. Producers SHOULD set `canonical_formats_only: true` rather than omit the declaration from `format_options` — explicit v2-only is more useful than silent absence.'
        ),
    ] = False
    experimental: Annotated[
        bool | None,
        Field(
            description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum."
        ),
    ] = False
    format_shape: Annotated[
        str | None,
        Field(
            description='REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. Recognized global pattern this custom shape is an instance of, drawn from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json) (`multi_placement_takeover`, `roadblock`, `branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values valid (validators MAY soft-warn) — adopters CAN ship a shape that isn\'t yet in the registry. Adding entries is a vocabulary PR. Once a `format_shape` entry sees 2+ adopters with substantively similar `format_schema` content for 90+ days, the working group promotes it to a first-class canonical.'
        ),
    ] = None
    v1_format_ref: Annotated[
        list[format_id.FormatReferenceStructuredObject] | None,
        Field(
            description="Authoritative v2 → v1 link, expressed as an array of one or more v1 `format_id` ({agent_url, id}) values. Each entry asserts that this canonical-formats declaration IS the same underlying format as the referenced v1 named format. Always an array (single-ref is `[{...}]`) so the multi-size case below has a clean wire shape — adopters surveyed in the SDK implementor review pushed for this over the lossy single-ref form.\n\nThe v2 declaration's `params` MUST narrow (be compatible with) each referenced v1 format's `requirements` — see the 'Narrows — formal definition' section in canonical-formats.mdx. SDKs comparing dual-emitted shapes (`Product.format_ids[]` ⊇ entries from `v1_format_ref` AND `Product.format_options[]` carrying this declaration) treat the link as the authoritative pairing and run the narrowing check between this declaration and EACH referenced v1 format file's `requirements`.\n\n**Multi-size fan-out (normative).** When the declaration carries `params.sizes: [{w,h}, ...]` (multi-size flexible slot), sellers SHOULD carry one `v1_format_ref[]` entry per size, each pointing at the per-size v1 named format in the AAO catalog. Example: a multi-size image declaration with `sizes: [300x250, 728x90, 970x250]` SHOULD carry `v1_format_ref: [{aao, display_300x250_image}, {aao, display_728x90_image}, {aao, display_970x250_image}]`. v1-only buyers then see the product on all three sizes via the `format_ids[]` dual-emission. When `v1_format_ref[]` count < `sizes[]` count, SDKs MUST emit `FORMAT_DECLARATION_V1_LOSSY_MULTI_SIZE` on the response `errors[]` (advisory, alongside the partial-coverage v1 emit — NOT in place of it). SDKs MAY (non-normative) fan out automatically by catalog lookup when `v1_format_ref[]` has length 1 and `sizes[]` has length N — opt-in, requires catalog access; sellers asserting refs is the source of truth.\n\nMutually exclusive with `canonical_formats_only: true` — a declaration can EITHER assert no v1 projection (`canonical_formats_only: true`) OR link to v1 named formats (`v1_format_ref[]`), never both. When neither is present, SDKs fall back to the resolution order in `v1-canonical-mapping.json` (seller's explicit `canonical` field on the v1 file → registry glob → structural match → fail-closed).\n\nThis is the v2-side authoritative replacement for the v1-side `canonical_parameters` field on `format.json` (which is deprecated for 3.1, removed at 4.0). Sellers SHOULD prefer authoring v2 declarations with `v1_format_ref[]` over mirroring the v2 shape onto v1 files via `canonical_parameters`; the directional link (v2 declaration → v1 identifiers) is the same fact without the parallel-shape drift surface.\n\n**AAO-hosted convention (normative).** For IAB-standard formats (image dimensions, VAST/DAAST tags, standard third-party tags, HTML5 banner bundles), sellers SHOULD point each `v1_format_ref[].agent_url` at the AAO-hosted canonical agent URL `https://creative.adcontextprotocol.org` and use the registry-published id (e.g., `display_300x250_image`, `video_vast_30s`, `audio_standard_30s`, `display_300x250_html`, `display_js`). This converges the v1-wire namespace: every seller's IAB MREC points at the same `{agent_url, id}` pair, so v1-only buyers' allowlists work uniformly. Without this convention, every publisher's 300x250 ships with a different `v1_format_ref` (theirs vs nytimes.example vs cnn.example vs …) and the v1 wire fragments into per-publisher namespaces — exactly what canonical-formats was designed to eliminate.\n\nFor platform-specific formats (Meta Reels, TikTok Spark, Snap Spotlight, etc.), each `v1_format_ref[].agent_url` SHOULD point at the platform's own agent_url when the platform has adopted AdCP and publishes its own `adagents.json` with `formats[]`. When the platform has NOT adopted AdCP, sellers SHOULD point at the AAO community-registry mirror — `https://creative.adcontextprotocol.org/translated/<platform>` + `id: <platform-format-name>` (e.g., `https://creative.adcontextprotocol.org/translated/meta` + `id: meta_reels`). This keeps the v1 namespace converged across all sellers selling that platform's inventory until the platform owns its own adagents.json.\n\n**Platform-adoption cutover (normative).** When a platform adopts AdCP and publishes its own adagents.json, sellers MUST update `v1_format_ref[].agent_url` to the platform's adopted agent_url in the same minor release as the AAO mirror entry's `superseded_by` field goes live (see `static/schemas/source/adagents.json#superseded_by`). The AAO mirror entry SHOULD continue serving for ≥1 minor release after `superseded_by` is set, returning an advisory 'superseded' marker so v1 buyer allowlists keyed on the mirror URL get an explicit signal rather than a silent break. **Identity-confusion note**: the mirror URL is *format-shape namespace*, NOT seller identity. Inventory authorization always flows from `authorized_agents[]` + publisher signing keys; a buyer matching `v1_format_ref[].agent_url` against an allowlist is matching format-shape provenance, not seller identity.\n\n**Mirror domain migration (3.1).** Earlier drafts used `https://mirror.adcontextprotocol.org/translated/<platform>`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/<platform>` — sibling content under the AAO catalog domain we already host. Adopters who hardcoded the earlier mirror URL MUST migrate to the new path; the canonical-formats.mdx migration section documents the move. No transitional redirect is currently published (the earlier subdomain was never provisioned).\n\nFor seller-bespoke formats (a publisher's `acme_homepage_takeover` that doesn't fit IAB conventions), each `v1_format_ref[].agent_url` is the seller's own agent_url and the id is seller-namespaced. These won't appear in `v1-canonical-mapping.json`'s registry; they're seller-asserted only.",
            min_length=1,
        ),
    ] = None
    format_schema: Annotated[
        platform_extension_ref.PlatformExtensionReference | None,
        Field(
            description='REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. URI+digest reference to a fetchable schema describing this custom shape\'s actual `params` and `slots`. Same hosting model as `platform_extensions`: open-ecosystem publishers host the artifact at the canonical URI on their subdomain; closed-platform / walled-garden shapes resolve through the AAO mirror at `https://creative.adcontextprotocol.org/translated/...`. Buyer agents fetch by `uri@digest` (immutable per digest, aggressive caching, `Cache-Control: public, max-age=31536000, immutable`), validate `params` and `slots` against the fetched schema, and reason about manifests structurally — same mechanic as platform_extensions but at the format-structure level. Without `format_schema`, custom shapes would be opaque to buyer agents and the protocol would regress to per-seller integration code; that\'s why the schema is required, not optional.\n\n**Fetch contract (normative)** — `format_schema` is load-bearing for validation (unlike `platform_extensions`, which is informational on the *consumption* side). The *transport* rules below apply identically to BOTH fields — any SDK fetching a `platform-extension-ref.json` URI MUST apply this contract regardless of whether the field name is `format_schema` or `platform_extensions`. A shared SDK fetch path that drops to the weakest bar undermines `format_schema`\'s hardening. The consumption distinction (load-bearing vs informational) is about *what the body means*; the transport distinction is `https`-and-allowlisted regardless.\n\n- **Transport**: `https` only. Buyers MUST reject `http://`, `file://`, `data:`, and any non-`https` scheme. The URI MUST resolve to a JSON document that is itself a valid JSON Schema (Draft 07 or 2020-12; producers MUST declare `$schema`).\n- **SSRF protection**: buyers MUST resolve the URI hostname and reject if any resolved address is in RFC 1918 private space (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`), loopback (`127.0.0.0/8`, `::1`), link-local (`169.254.0.0/16`, `fe80::/10`), CGNAT (`100.64.0.0/10`), or any RFC 6761 special-use name (`.local`, `.localhost`, `.internal`, `.test`, `.example`, `.invalid`). Cloud metadata endpoints (`169.254.169.254`, `metadata.google.internal`, `kubernetes.default.svc`) are explicitly forbidden — these are credential-leak primitives. Buyers MUST pin the connection to the resolved IP (or re-resolve and re-validate the allowlist per request) to defeat DNS rebinding.\n- **HTTP redirects**: MUST be disabled. If a follow is implemented at all, the redirect target MUST pass the same scheme + SSRF + allowlist checks; otherwise the fetch hard-fails. Open redirects on same-origin paths are otherwise a free SSRF primitive.\n- **Response size cap**: response body MUST be capped at 1 MiB. Enforce during streaming, not after full buffering. Over-cap hard-fails identically to digest mismatch.\n- **Timeout**: SDKs SHOULD apply a fetch timeout ≤5 seconds. Timeout SHOULD be treated identically to an HTTP 5xx response (transient — retry policy at the SDK\'s discretion; on persistent failure surface as unresolved and skip the declaration for this session).\n- **Digest verification**: SHA-256 of the response body MUST equal `digest`. **Digest mismatch is a hard fail** — the buyer MUST treat the format declaration as unresolvable and MUST NOT validate manifests against the mismatched body. A divergent digest is either a malicious substitution or producer error; either way, falling back to the un-verified body breaks the trust model. Digest format: `sha256:` prefix + 64 lowercase hex characters. Cache key is `uri@digest`; digest mismatch MUST NOT be cached as a negative result keyed on `uri` alone (defeats CDN-flap recovery), and MUST be distinguishable in telemetry from network 5xx / 404 (sustained mismatch is a substitution-attack signal, not a flap).\n- **Sandboxing of `$ref`**: fetched schemas MAY use `$ref`. Buyers MUST resolve `$ref` only to URIs that are (a) same-origin as the parent `format_schema.uri` after RFC 3986 §6 normalization (lowercase scheme + host, strip default port, normalize path dot-segments, no userinfo component), OR (b) hosted under the AAO catalog domain (`https://creative.adcontextprotocol.org/...`), OR (c) intra-document JSON Pointer refs (`#/...`) bounded to the parent document\'s parsed tree. Cross-origin `$ref` to arbitrary URIs MUST be rejected. `$ref: file://...` MUST be rejected unconditionally. Transitive `$ref` chains MUST be bounded at depth ≤8 AND `$ref` count ≤256 across the resolved tree (depth 8 with breadth 100 per level is 10^16 nodes — depth alone is not enough). Publishers SHOULD inline rather than $ref where possible.\n- **Schema-compile bounds (DoS protection)**: validators MUST bound CPU/memory on fetched schemas. Recommended: compiled-schema keyword count ≤10 000, `pattern` regexes evaluated with a non-backtracking engine (re2) OR under a per-pattern timeout, per-manifest validation budget ≤250 ms (exceeded budget → treat manifest as invalid, surface telemetry signal). Without these, a \'valid\' schema with catastrophic regex backtracking or exponential `allOf`/`anyOf` expansion pins a CPU forever.\n- **Cache**: buyers cache fetched schemas by `uri@digest` and treat them as immutable (the same hosting contract as `platform_extensions`). On `404`, network partition, or persistent fetch failure, buyers SHOULD degrade gracefully (treat the declaration as unresolved, skip it for the current `get_products` response, surface via `errors[]` with the relevant code) rather than failing the entire session.\n- **Schema-not-valid handling**: if the fetched body parses as JSON but is not a valid JSON Schema, the buyer MUST treat the declaration as unresolvable (same as digest mismatch) and surface via `errors[]`. Validators MUST NOT attempt partial validation against an invalid schema.\n- **AAO catalog trust**: `https://creative.adcontextprotocol.org/*` is a single trust anchor in the same-origin allowlist; compromise of the catalog domain or its CA compromises every buyer agent. Catalog-served bodies MUST be digest-pinned identically to origin fetches (the digest is on the *parent* `format_schema.uri@digest`, not on the catalog response). Future hardening (signed bodies, transparency log) is tracked separately.'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var applies_to_channels : list[adcp.types.generated_poc.enums.channels.MediaChannel] | None
var canonical_formats_only : bool | None
var display_name : str | None
var experimental : bool | None
var format_option_id : str | None
var format_schema : adcp.types.generated_poc.core.platform_extension_ref.PlatformExtensionReference | None
var format_shape : str | None
var locale_policy : adcp.types.generated_poc.core.creative_locale_policy.CreativeLocalePolicy | None
var model_config
var publisher_domain : str | None
var sample_render_url : pydantic.networks.AnyUrl | None
var seller_preference : adcp.types.generated_poc.core.product_format_declaration.SellerPreference | None
var v1_format_ref : list[adcp.types.generated_poc.core.format_id.FormatReferenceStructuredObject] | None

Inherited members

class ProductSignalTargetingOption (**data: Any)
Expand source code
class ProductSignalTargetingOption(SignalListing):
    model_config = ConfigDict(
        extra='allow',
    )
    signal_agent_segment_id: Annotated[
        str | None,
        Field(
            description='Optional opaque resolved-segment or seller execution handle for this signal. Omit when signal_ref plus the value expression is sufficient for the seller to resolve the signal. Include when the seller exposes a distinct runtime or activation handle that buyers must echo in packages[].targeting_overlay.signal_targeting_groups.groups[].signals[].signal_agent_segment_id. Buyers SHOULD echo this handle verbatim rather than reconstructing identity from categorical values; providers MAY namespace handles so cross-provider identity stays legible without a shared taxonomy registry.'
        ),
    ] = None
    activation_status: Annotated[
        ActivationStatus | None,
        Field(
            description="Whether this signal option is ready to select on create_media_buy for the requesting account. 'ready' means the buyer can select it directly. 'requires_activation' means the buyer must activate the signal first or include an activation_key the seller accepts."
        ),
    ] = ActivationStatus.ready
    allowed_targeting_modes: Annotated[
        list[AllowedTargetingMode] | None,
        Field(
            description="How this signal may be used when composing package-level signal targeting groups. 'include' means the signal may appear in an 'any' child group. 'exclude' means the signal may appear in a 'none' child group. Omit when the signal is include-only. This field declares the allowed buy-time group operator; binary package signal entries still use value=true in both include and exclude groups.",
            min_length=1,
        ),
    ] = [AllowedTargetingMode.include]
    default_selected: Annotated[
        bool | None,
        Field(
            description="Whether the seller recommends or preselects this signal when composing this product. Buyers may remove it unless signal_targeting_rules.selection_mode is 'fixed'. When selection_mode is 'fixed', sellers apply default_selected signals even if the buyer omits signal_targeting_groups and MUST echo the applied entries on the resulting package state."
        ),
    ] = False
    selection_group: Annotated[
        str | None,
        Field(
            description='Optional product-defined composability bucket for signal options, such as alternative audience tiers, a key-value targeting plane, or an audience-segment targeting plane. Signals in the same selection_group are expected to be OR-combinable inside one child group for a given targeting mode, subject to signal_targeting_rules. Use different selection_group values when the product requires separate ANDed clauses, such as signal sets backed by different platform targeting primitives that cannot be collapsed into one child group. selection_group is a product-option grouping key, not a reference to one child object in packages[].targeting_overlay.signal_targeting_groups.groups[]. Sellers can use signal_targeting_rules.max_selected_per_group and signal_targeting_rules.selection_group_rules with selection_group to guide and validate storefront composition.'
        ),
    ] = None
    pricing_options: Annotated[
        list[vendor_pricing_option.VendorPricingOption] | None,
        Field(
            description='Signal pricing options available when this signal is selected on this product. Product-scoped pricing is authoritative for this product; if get_signals exposes a different default rate card, use this product-scoped price when composing the buy. Buyers pass the selected pricing_option_id in packages[].targeting_overlay.signal_targeting_groups.groups[].signals[].pricing_option_id. Omit when the signal is bundled into the product price or has no incremental cost.',
            min_length=1,
        ),
    ] = None
    signal_ref: Annotated[
        signal_ref.SignalRef,
        Field(
            description="Canonical signal reference. Use scope 'product' for a product-local signal defined by this listing; use scope 'data_provider' with data_provider_domain for a signal defined in a data provider's published adagents.json signals[]; use scope 'signal_source' with signal_source_url for a source-native signal."
        ),
    ]

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.signal_listing.SignalListing
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var activation_status : adcp.types.generated_poc.core.product_signal_targeting_option.ActivationStatus | None
var allowed_targeting_modes : list[adcp.types.generated_poc.core.product_signal_targeting_option.AllowedTargetingMode] | None
var default_selected : bool | None
var model_config
var pricing_options : list[adcp.types.generated_poc.core.vendor_pricing_option.VendorPricingOption] | None
var selection_group : str | None
var signal_agent_segment_id : str | None
var signal_ref : adcp.types.generated_poc.core.signal_ref.SignalRef

Inherited members

class Property (**data: Any)
Expand source code
class Property(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    property_id: Annotated[
        property_id_1.PropertyId | None,
        Field(
            description='Unique identifier for this property (optional). Enables referencing properties by ID instead of repeating full objects.'
        ),
    ] = None
    property_type: Annotated[
        property_type_1.PropertyType, Field(description='Type of advertising property')
    ]
    name: Annotated[str, Field(description='Human-readable property name')]
    identifiers: Annotated[
        list[Identifier], Field(description='Array of identifiers for this property', min_length=1)
    ]
    tags: Annotated[
        list[property_tag.PropertyTag] | None,
        Field(
            description='Tags for categorization and grouping (e.g., network membership, content categories)'
        ),
    ] = None
    supported_channels: Annotated[
        list[channels.MediaChannel] | None,
        Field(
            description="Advertising channels this property supports (e.g., ['display', 'olv', 'social']). Publishers declare which channels their inventory aligns with. Properties may support multiple channels. See the Media Channel Taxonomy for definitions."
        ),
    ] = None
    publisher_domain: Annotated[
        str | None,
        Field(
            description='Domain where adagents.json should be checked for authorization validation. Optional in adagents.json (file location implies domain).'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var identifiers : list[adcp.types.generated_poc.core.property.Identifier]
var model_config
var name : str
var property_id : adcp.types.generated_poc.core.property_id.PropertyId | None
var property_type : adcp.types.generated_poc.enums.property_type.PropertyType
var publisher_domain : str | None
var supported_channels : list[adcp.types.generated_poc.enums.channels.MediaChannel] | None
var tags : list[adcp.types.generated_poc.core.property_tag.PropertyTag] | None

Inherited members

class PropertyActivity (**data: Any)
Expand source code
class PropertyActivity(RegistryBaseModel):
    domain: Annotated[str, Field(examples=["examplepub.com"])]
    total: Annotated[int, Field(examples=[3])]
    revisions: list[ActivityRevision]

Base model for registry API types.

Uses extra='allow' so that new fields from the registry API are preserved rather than dropped. This differs from AdCPBaseModel which defaults to extra='ignore' for protocol types.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var domain : str
var model_config
var revisions : list[ActivityRevision]
var total : int
class PropertyId (root: RootModelRootType = PydanticUndefined, **data)
Expand source code
class PropertyId(RootModel[str]):
    root: Annotated[
        str,
        Field(
            description='Identifier for a publisher property. Must be lowercase alphanumeric with underscores only.',
            examples=['cnn_ctv_app', 'homepage', 'mobile_ios', 'instagram'],
            pattern='^[a-z0-9_]+$',
            title='Property ID',
        ),
    ]

Usage Documentation

RootModel and Custom Root Types

A Pydantic BaseModel for the root object of the model.

Attributes
-----=
root
The root object of the model.
__pydantic_root_model__
Whether the model is a RootModel.
__pydantic_private__
Private fields in the model.
__pydantic_extra__
Extra fields in the model.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • pydantic.root_model.RootModel[str]
  • pydantic.root_model.RootModel
  • pydantic.main.BaseModel
  • typing.Generic

Class variables

var model_config
var root : str
class PropertyIdentifier (**data: Any)
Expand source code
class PropertyIdentifier(RegistryBaseModel):
    type: Annotated[str, Field(examples=["domain"])]
    value: Annotated[str, Field(examples=["examplepub.com"])]

Base model for registry API types.

Uses extra='allow' so that new fields from the registry API are preserved rather than dropped. This differs from AdCPBaseModel which defaults to extra='ignore' for protocol types.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var model_config
var type : str
var value : str
class PropertyRegistry (client: RegistryClient,
*,
auth_token: str | None = None,
poll_interval: float = 60.0,
cursor_store: CursorStore | None = None)
Expand source code
class PropertyRegistry:
    """Local cache of property/agent authorization relationships.

    Queries are synchronous dict lookups — no network calls.
    Background sync is opt-in via ``auth_token``.

    Args:
        client: RegistryClient for API calls.
        auth_token: Bearer token for change feed access.
            If omitted, background sync is disabled (load-only mode).
        poll_interval: Seconds between feed polls (default 60).
        cursor_store: Optional CursorStore for feed cursor persistence.
    """

    def __init__(
        self,
        client: RegistryClient,
        *,
        auth_token: str | None = None,
        poll_interval: float = 60.0,
        cursor_store: CursorStore | None = None,
    ) -> None:
        self._client = client
        self._auth_token = auth_token
        self._poll_interval = poll_interval
        self._cursor_store = cursor_store
        self._domain_to_agents: dict[str, set[str]] = {}
        self._agent_to_domains: dict[str, set[str]] = {}
        self._loaded = False
        self._sync: RegistrySync | None = None
        self._task: asyncio.Task[None] | None = None

    # ------------------------------------------------------------------
    # Queries (synchronous, no network)
    # ------------------------------------------------------------------

    def is_authorized(self, agent_url: str, domain: str) -> bool:
        """Check if an agent is authorized for a domain."""
        return agent_url in self._domain_to_agents.get(domain, set())

    def get_domains(self, agent_url: str) -> frozenset[str]:
        """Get all domains authorized for an agent."""
        return frozenset(self._agent_to_domains.get(agent_url, set()))

    def get_agents(self, domain: str) -> frozenset[str]:
        """Get all agents authorized for a domain."""
        return frozenset(self._domain_to_agents.get(domain, set()))

    @property
    def agent_count(self) -> int:
        """Number of agents in the index."""
        return len(self._agent_to_domains)

    @property
    def domain_count(self) -> int:
        """Number of domains in the index."""
        return len(self._domain_to_agents)

    @property
    def loaded(self) -> bool:
        """Whether initial data has been loaded."""
        return self._loaded

    # ------------------------------------------------------------------
    # Lifecycle
    # ------------------------------------------------------------------

    async def load(self) -> None:
        """Fetch initial state from the registry API.

        Calls ``list_agents()`` and builds the bidirectional
        authorization index from each agent's ``publisher_domains``.
        """
        agents = await self._client.list_agents(properties=True)
        domain_to_agents: dict[str, set[str]] = {}
        agent_to_domains: dict[str, set[str]] = {}

        for agent in agents:
            domains = agent.publisher_domains or []
            if domains:
                agent_to_domains[agent.url] = set(domains)
                for domain in domains:
                    domain_to_agents.setdefault(domain, set()).add(agent.url)

        self._domain_to_agents = domain_to_agents
        self._agent_to_domains = agent_to_domains
        self._loaded = True
        logger.info(
            "PropertyRegistry loaded: %d agents, %d domains",
            len(agent_to_domains),
            len(domain_to_agents),
        )

    async def start(self) -> None:
        """Load initial state and start background sync.

        If ``auth_token`` was not provided, only loads initial state
        without starting the polling loop.
        """
        if not self._loaded:
            await self.load()

        if self._auth_token is None:
            logger.info(
                "PropertyRegistry: no auth_token, background sync disabled"
            )
            return

        self._sync = RegistrySync(
            self._client,
            auth_token=self._auth_token,
            poll_interval=self._poll_interval,
            cursor_store=self._cursor_store,
            types="authorization.*,agent.*,property.*",
        )
        self._sync.on_all(self._handle_event)
        self._task = asyncio.create_task(self._sync.start())

    async def stop(self) -> None:
        """Stop background sync."""
        if self._sync is not None:
            await self._sync.stop()
        if self._task is not None:
            await self._task
            self._task = None
        self._sync = None

    async def __aenter__(self) -> PropertyRegistry:
        await self.start()
        return self

    async def __aexit__(self, *args: object) -> None:
        await self.stop()

    async def refresh(self) -> None:
        """Force a full reload from the API."""
        self._domain_to_agents.clear()
        self._agent_to_domains.clear()
        self._loaded = False
        await self.load()

    # ------------------------------------------------------------------
    # Event handling
    #
    # Trust model: events are fetched over HTTPS from the registry API
    # using a Bearer token. The events are not cryptographically signed.
    # A compromised transport or registry could inject forged events.
    # ------------------------------------------------------------------

    async def _handle_event(self, event: FeedEvent) -> None:
        """Route feed events to the appropriate handler."""
        et = event.event_type
        if et.startswith("authorization."):
            self._apply_authorization(event)
        elif et == "agent.deleted":
            self._remove_agent(event.entity_id)
        elif et in ("agent.created", "agent.updated"):
            await self._refresh_agent(event.payload.get("url", event.entity_id))
        elif et == "property.deleted":
            self._remove_domain(event.entity_id)
        # Unknown event types: ignore silently (forward compatible)

    _ADD_TYPES = {"authorization.created", "authorization.granted"}
    _REMOVE_TYPES = {"authorization.revoked", "authorization.deleted"}

    def _apply_authorization(self, event: FeedEvent) -> None:
        """Add or remove an authorization edge."""
        agent_url = event.payload.get("agent_url", "")
        domain = event.payload.get("domain", "")
        if not agent_url or not domain:
            return

        if event.event_type in self._ADD_TYPES:
            self._domain_to_agents.setdefault(domain, set()).add(agent_url)
            self._agent_to_domains.setdefault(agent_url, set()).add(domain)
        elif event.event_type in self._REMOVE_TYPES:
            self._domain_to_agents.get(domain, set()).discard(agent_url)
            self._agent_to_domains.get(agent_url, set()).discard(domain)

    def _remove_agent(self, agent_url: str) -> None:
        """Remove all authorization edges for an agent."""
        domains = self._agent_to_domains.pop(agent_url, set())
        for domain in domains:
            agents = self._domain_to_agents.get(domain)
            if agents is not None:
                agents.discard(agent_url)
                if not agents:
                    del self._domain_to_agents[domain]

    def _remove_domain(self, domain: str) -> None:
        """Remove all authorization edges for a domain."""
        agents = self._domain_to_agents.pop(domain, set())
        for agent_url in agents:
            domains = self._agent_to_domains.get(agent_url)
            if domains is not None:
                domains.discard(domain)
                if not domains:
                    del self._agent_to_domains[agent_url]

    async def _refresh_agent(self, agent_url: str) -> None:
        """Re-fetch a single agent's domains and update indexes."""
        try:
            data = await self._client.get_agent_domains(agent_url)
            new_domains = {
                p["domain"]
                for p in data.get("properties", [])
                if "domain" in p
            }
        except Exception as exc:
            logger.warning("Failed to refresh agent %s: %s", agent_url, exc)
            return

        # Remove old edges
        old_domains = self._agent_to_domains.get(agent_url, set())
        for d in old_domains:
            s = self._domain_to_agents.get(d)
            if s is not None:
                s.discard(agent_url)
                if not s:
                    del self._domain_to_agents[d]

        # Add new edges
        if new_domains:
            self._agent_to_domains[agent_url] = new_domains
            for d in new_domains:
                self._domain_to_agents.setdefault(d, set()).add(agent_url)
        else:
            self._agent_to_domains.pop(agent_url, None)

Local cache of property/agent authorization relationships.

Queries are synchronous dict lookups — no network calls. Background sync is opt-in via auth_token.

Args
-----=
client
RegistryClient for API calls.
auth_token
Bearer token for change feed access. If omitted, background sync is disabled (load-only mode).
poll_interval
Seconds between feed polls (default 60).
cursor_store
Optional CursorStore for feed cursor persistence.

Instance variables

prop agent_count : int
Expand source code
@property
def agent_count(self) -> int:
    """Number of agents in the index."""
    return len(self._agent_to_domains)

Number of agents in the index.

prop domain_count : int
Expand source code
@property
def domain_count(self) -> int:
    """Number of domains in the index."""
    return len(self._domain_to_agents)

Number of domains in the index.

prop loaded : bool
Expand source code
@property
def loaded(self) -> bool:
    """Whether initial data has been loaded."""
    return self._loaded

Whether initial data has been loaded.

Methods

def get_agents(self, domain: str) ‑> frozenset[str]
Expand source code
def get_agents(self, domain: str) -> frozenset[str]:
    """Get all agents authorized for a domain."""
    return frozenset(self._domain_to_agents.get(domain, set()))

Get all agents authorized for a domain.

def get_domains(self, agent_url: str) ‑> frozenset[str]
Expand source code
def get_domains(self, agent_url: str) -> frozenset[str]:
    """Get all domains authorized for an agent."""
    return frozenset(self._agent_to_domains.get(agent_url, set()))

Get all domains authorized for an agent.

def is_authorized(self, agent_url: str, domain: str) ‑> bool
Expand source code
def is_authorized(self, agent_url: str, domain: str) -> bool:
    """Check if an agent is authorized for a domain."""
    return agent_url in self._domain_to_agents.get(domain, set())

Check if an agent is authorized for a domain.

async def load(self) ‑> None
Expand source code
async def load(self) -> None:
    """Fetch initial state from the registry API.

    Calls ``list_agents()`` and builds the bidirectional
    authorization index from each agent's ``publisher_domains``.
    """
    agents = await self._client.list_agents(properties=True)
    domain_to_agents: dict[str, set[str]] = {}
    agent_to_domains: dict[str, set[str]] = {}

    for agent in agents:
        domains = agent.publisher_domains or []
        if domains:
            agent_to_domains[agent.url] = set(domains)
            for domain in domains:
                domain_to_agents.setdefault(domain, set()).add(agent.url)

    self._domain_to_agents = domain_to_agents
    self._agent_to_domains = agent_to_domains
    self._loaded = True
    logger.info(
        "PropertyRegistry loaded: %d agents, %d domains",
        len(agent_to_domains),
        len(domain_to_agents),
    )

Fetch initial state from the registry API.

Calls list_agents() and builds the bidirectional authorization index from each agent's publisher_domains.

async def refresh(self) ‑> None
Expand source code
async def refresh(self) -> None:
    """Force a full reload from the API."""
    self._domain_to_agents.clear()
    self._agent_to_domains.clear()
    self._loaded = False
    await self.load()

Force a full reload from the API.

async def start(self) ‑> None
Expand source code
async def start(self) -> None:
    """Load initial state and start background sync.

    If ``auth_token`` was not provided, only loads initial state
    without starting the polling loop.
    """
    if not self._loaded:
        await self.load()

    if self._auth_token is None:
        logger.info(
            "PropertyRegistry: no auth_token, background sync disabled"
        )
        return

    self._sync = RegistrySync(
        self._client,
        auth_token=self._auth_token,
        poll_interval=self._poll_interval,
        cursor_store=self._cursor_store,
        types="authorization.*,agent.*,property.*",
    )
    self._sync.on_all(self._handle_event)
    self._task = asyncio.create_task(self._sync.start())

Load initial state and start background sync.

If auth_token was not provided, only loads initial state without starting the polling loop.

async def stop(self) ‑> None
Expand source code
async def stop(self) -> None:
    """Stop background sync."""
    if self._sync is not None:
        await self._sync.stop()
    if self._task is not None:
        await self._task
        self._task = None
    self._sync = None

Stop background sync.

class PropertyRegistryItem (**data: Any)
Expand source code
class PropertyRegistryItem(RegistryBaseModel):
    domain: Annotated[str, Field(examples=["examplepub.com"])]
    source: PropertyRegistrySource
    property_count: int
    agent_count: int
    verified: bool

Base model for registry API types.

Uses extra='allow' so that new fields from the registry API are preserved rather than dropped. This differs from AdCPBaseModel which defaults to extra='ignore' for protocol types.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var agent_count : int
var domain : str
var model_config
var property_count : int
var sourcePropertyRegistrySource
var verified : bool
class PropertySummary (**data: Any)
Expand source code
class PropertySummary(RegistryBaseModel):
    total_count: int
    count_by_type: dict[str, int]
    tags: list[str]
    publisher_count: int

Base model for registry API types.

Uses extra='allow' so that new fields from the registry API are preserved rather than dropped. This differs from AdCPBaseModel which defaults to extra='ignore' for protocol types.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var count_by_type : dict[str, int]
var model_config
var publisher_count : int
var tags : list[str]
var total_count : int
class PropertyTag (root: RootModelRootType = PydanticUndefined, **data)
Expand source code
class PropertyTag(RootModel[str]):
    root: Annotated[
        str,
        Field(
            description='Tag for categorizing publisher properties. Must be lowercase alphanumeric with underscores only.',
            examples=['ctv', 'premium', 'news', 'sports', 'meta_network', 'social_media'],
            pattern='^[a-z0-9_]+$',
            title='Property Tag',
        ),
    ]

Usage Documentation

RootModel and Custom Root Types

A Pydantic BaseModel for the root object of the model.

Attributes
-----=
root
The root object of the model.
__pydantic_root_model__
Whether the model is a RootModel.
__pydantic_private__
Private fields in the model.
__pydantic_extra__
Extra fields in the model.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • pydantic.root_model.RootModel[str]
  • pydantic.root_model.RootModel
  • pydantic.main.BaseModel
  • typing.Generic

Class variables

var model_config
var root : str
class Proposal (**data: Any)
Expand source code
class Proposal(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    proposal_id: Annotated[
        str,
        Field(
            description='Unique identifier for this proposal. Used to finalize a draft proposal and to execute a committed proposal via create_media_buy.',
            max_length=255,
        ),
    ]
    name: Annotated[
        str, Field(description='Human-readable name for this media plan proposal', max_length=500)
    ]
    description: Annotated[
        str | None,
        Field(
            description='Explanation of the proposal strategy and what it achieves', max_length=2000
        ),
    ] = None
    allocations: Annotated[
        list[product_allocation.ProductAllocation],
        Field(
            description='Products and budget constraints in this plan. Fixed proposals require allocation_percentage on every entry and percentages MUST sum to 100. Seller-optimized proposals forbid exact allocation_percentage and may instead supply min_spend_target_percentage and max_spend_percentage. Publishers are responsible for validating cross-entry sums; buyers SHOULD validate them before execution.',
            min_length=1,
        ),
    ]
    budget_allocation: Annotated[
        budget_allocation_1.BudgetAllocation | None,
        Field(
            description='How the executed total budget is allocated across proposal products. Omit for legacy fixed proposals.'
        ),
    ] = None
    pacing: Annotated[
        pacing_1.Pacing | None,
        Field(
            description='Recommended aggregate pacing for the executed media-buy budget. On a committed proposal this is part of the firm delivery terms.'
        ),
    ] = None
    proposal_status: Annotated[
        proposal_status_1.ProposalStatus | None,
        Field(
            description="Lifecycle status of this proposal and the per-proposal source of truth for whether finalization is required before create_media_buy. When absent, the proposal is ready to buy (backward compatible). 'draft' means indicative pricing — finalize via refine before purchasing. 'committed' means firm pricing with inventory reserved until expires_at and executable via create_media_buy."
        ),
    ] = None
    expires_at: Annotated[
        AwareDatetime | None,
        Field(
            description='When this proposal expires and can no longer be executed. For draft proposals, indicates when indicative pricing becomes stale. For committed proposals, indicates when the inventory hold lapses — the buyer must call create_media_buy before this time.'
        ),
    ] = None
    insertion_order: Annotated[
        insertion_order_1.InsertionOrder | None,
        Field(
            description='Formal insertion order attached to a committed proposal. Present when the seller requires a signed agreement before the media buy can proceed. The buyer references the io_id in io_acceptance on create_media_buy.'
        ),
    ] = None
    total_budget_guidance: Annotated[
        TotalBudgetGuidance | None, Field(description='Optional budget guidance for this proposal')
    ] = None
    brief_alignment: Annotated[
        str | None,
        Field(
            description='Explanation of how this proposal aligns with the campaign brief',
            max_length=2000,
        ),
    ] = None
    forecast: Annotated[
        delivery_forecast.DeliveryForecast | None,
        Field(
            description='Aggregate forecasted delivery metrics for the entire proposal. When both proposal-level and allocation-level forecasts are present, the proposal-level forecast is authoritative for total delivery estimation.'
        ),
    ] = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var allocations : list[adcp.types.generated_poc.core.product_allocation.ProductAllocation]
var brief_alignment : str | None
var budget_allocation : adcp.types.generated_poc.core.budget_allocation.BudgetAllocation | None
var description : str | None
var expires_at : pydantic.types.AwareDatetime | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var forecast : adcp.types.generated_poc.core.delivery_forecast.DeliveryForecast | None
var insertion_order : adcp.types.generated_poc.core.insertion_order.InsertionOrder | None
var model_config
var name : str
var pacing : adcp.types.generated_poc.enums.pacing.Pacing | None
var proposal_id : str
var proposal_status : adcp.types.generated_poc.enums.proposal_status.ProposalStatus | None
var total_budget_guidance : adcp.types.generated_poc.core.proposal.TotalBudgetGuidance | None

Inherited members

class Protocol (*args, **kwds)
Expand source code
class Protocol(str, Enum):
    """Supported protocols."""

    A2A = "a2a"
    MCP = "mcp"

Supported protocols.

Ancestors

  • builtins.str
  • enum.Enum

Class variables

var A2A
var MCP
class ProvidePerformanceFeedbackRequest (**data: Any)
Expand source code
class ProvidePerformanceFeedbackRequest(AdcpVersionEnvelope, PerformanceFeedbackAssertion):
    model_config = ConfigDict(
        extra='allow',
    )
    idempotency_key: Annotated[
        str,
        Field(
            description='Client-generated unique key for this logical assertion. MUST be unique per receiving agent to prevent cross-agent correlation; use a fresh UUID v4 for each new assertion. Retries use the same key and payload.',
            max_length=255,
            min_length=16,
            pattern='^[A-Za-z0-9_.:-]{16,255}$',
        ),
    ]
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • adcp.types.generated_poc.core.performance_feedback_assertion.PerformanceFeedbackAssertion
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var idempotency_key : str
var model_config
class ProvidePerformanceFeedbackByBuyerRefRequest (**data: Any)
Expand source code
class ProvidePerformanceFeedbackRequest(AdcpVersionEnvelope, PerformanceFeedbackAssertion):
    model_config = ConfigDict(
        extra='allow',
    )
    idempotency_key: Annotated[
        str,
        Field(
            description='Client-generated unique key for this logical assertion. MUST be unique per receiving agent to prevent cross-agent correlation; use a fresh UUID v4 for each new assertion. Retries use the same key and payload.',
            max_length=255,
            min_length=16,
            pattern='^[A-Za-z0-9_.:-]{16,255}$',
        ),
    ]
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • adcp.types.generated_poc.core.performance_feedback_assertion.PerformanceFeedbackAssertion
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var idempotency_key : str
var model_config
class ProvidePerformanceFeedbackByMediaBuyRequest (**data: Any)
Expand source code
class ProvidePerformanceFeedbackRequest(AdcpVersionEnvelope, PerformanceFeedbackAssertion):
    model_config = ConfigDict(
        extra='allow',
    )
    idempotency_key: Annotated[
        str,
        Field(
            description='Client-generated unique key for this logical assertion. MUST be unique per receiving agent to prevent cross-agent correlation; use a fresh UUID v4 for each new assertion. Retries use the same key and payload.',
            max_length=255,
            min_length=16,
            pattern='^[A-Za-z0-9_.:-]{16,255}$',
        ),
    ]
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • adcp.types.generated_poc.core.performance_feedback_assertion.PerformanceFeedbackAssertion
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var idempotency_key : str
var model_config

Inherited members

class ProvidePerformanceFeedbackSuccessResponse (**data: Any)
Expand source code
class ProvidePerformanceFeedbackResponse1(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    success: Literal[True]
    feedback_id: Annotated[str, StringConstraints(min_length=1)] | None = None
    application_status: Literal['accepted', 'applied', 'not_applied'] | None = None
    status_reason: Annotated[str, StringConstraints(max_length=500)] | None = None
    received_at: AwareDatetime | None = None
    applied_at: AwareDatetime | None = None
    sandbox: bool | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var application_status : Literal['accepted', 'applied', 'not_applied'] | None
var applied_at : pydantic.types.AwareDatetime | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var feedback_id : str | None
var model_config
var received_at : pydantic.types.AwareDatetime | None
var sandbox : bool | None
var status_reason : str | None
var success : Literal[True]
class ProvidePerformanceFeedbackResponse1 (**data: Any)
Expand source code
class ProvidePerformanceFeedbackResponse1(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    success: Literal[True]
    feedback_id: Annotated[str, StringConstraints(min_length=1)] | None = None
    application_status: Literal['accepted', 'applied', 'not_applied'] | None = None
    status_reason: Annotated[str, StringConstraints(max_length=500)] | None = None
    received_at: AwareDatetime | None = None
    applied_at: AwareDatetime | None = None
    sandbox: bool | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var application_status : Literal['accepted', 'applied', 'not_applied'] | None
var applied_at : pydantic.types.AwareDatetime | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var feedback_id : str | None
var model_config
var received_at : pydantic.types.AwareDatetime | None
var sandbox : bool | None
var status_reason : str | None
var success : Literal[True]

Inherited members

class ProvidePerformanceFeedbackErrorResponse (**data: Any)
Expand source code
class ProvidePerformanceFeedbackResponse2(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    errors: Annotated[list[error_1.Error], Field(min_length=1)]
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var errors : list[adcp.types.generated_poc.core.error.Error]
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config

Inherited members

class PublishedPostAsset (**data: Any)
Expand source code
class PublishedPostAsset(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    asset_type: Annotated[
        Literal['published_post'],
        Field(
            description='Discriminator identifying this as a published-post reference asset. See /schemas/creative/asset-types for the registry.'
        ),
    ] = 'published_post'
    post_url: Annotated[
        AnyUrl | None,
        Field(
            description='Canonical URL for the published post. Preferred when the platform exposes a stable public or authenticated URL.'
        ),
    ] = None
    platform: Annotated[
        str | None,
        Field(
            description="Optional platform or publisher namespace for the referenced post. Informational unless the seller's product declaration or platform extension narrows the accepted values."
        ),
    ] = None
    platform_post_id: Annotated[
        str | None,
        Field(
            description='Optional platform-native post identifier when a URL alone is not stable or not available. Buyers SHOULD include `platform` when using `platform_post_id` without `post_url`, unless the product or format declaration already narrows the platform. Platform-specific identifier semantics belong in platform_extensions; this field is only an opaque reference.'
        ),
    ] = None
    identity_ref: Annotated[
        IdentityRef | None,
        Field(
            description='Optional identity hint for the authoring handle/page/channel that owns the post. Sellers MUST verify authorization from platform state; buyers MUST NOT use this object as proof of authorization.'
        ),
    ] = None
    published_at: Annotated[
        AwareDatetime | None,
        Field(description='When the referenced post was originally published, if known.'),
    ] = None
    reference_authorization: Annotated[
        ReferenceAuthorization | None,
        Field(
            description='Server-emitted, seller-observed authorization state for the referenced post or identity. Sellers MAY return this object on read surfaces. On write requests, sellers MUST ignore buyer-supplied `reference_authorization.status` and other authorization-state claims unless a platform extension explicitly defines a signed proof shape and the seller verifies that proof.'
        ),
    ] = None
    provenance: Annotated[
        provenance_1.Provenance | None,
        Field(
            description='Provenance metadata for this reference asset, overrides manifest-level provenance.'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var asset_type : Literal['published_post']
var identity_ref : adcp.types.generated_poc.core.assets.published_post_asset.IdentityRef | None
var model_config
var platform : str | None
var platform_post_id : str | None
var post_url : pydantic.networks.AnyUrl | None
var provenance : adcp.types.generated_poc.core.provenance.Provenance | None
var published_at : pydantic.types.AwareDatetime | None
var reference_authorization : adcp.types.generated_poc.core.assets.published_post_asset.ReferenceAuthorization | None

Inherited members

class PublisherDesignatedPreviewProvider (**data: Any)
Expand source code
class PublisherDesignatedPreviewProvider(AdCPBaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    agent_url: Annotated[
        AnyUrl,
        Field(
            description='HTTPS URL of the delegated creative-agent endpoint. Buyers call get_adcp_capabilities and preview_creative on this endpoint. They MUST allow only public IPs, pin DNS resolution through connection, refuse redirects, cap time and response size, and attach provider credentials only after exact normalized-origin binding.'
        ),
    ]
    authority: Annotated[
        Literal['publisher_designated'],
        Field(
            description="Explicitly states that authority comes from the publisher-hosted placement declaration. The provider's rendering_origin metadata is informational and remains non-authoritative elsewhere."
        ),
    ] = 'publisher_designated'
    routes: Annotated[list[Route], Field(min_length=1)]

    @field_validator('agent_url')
    @classmethod
    def _require_https_agent_url(cls, value: AnyUrl) -> AnyUrl:
        if value.scheme != 'https':
            raise ValueError('agent_url must use https')
        return value

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var agent_url : pydantic.networks.AnyUrl
var authority : Literal['publisher_designated']
var model_config
var routes : list[adcp.types.generated_poc.core.preview_provider.Route]

Inherited members

class PublisherDivergence (**data: Any)
Expand source code
class PublisherDivergence(AdCPBaseModel):
    """Divergence record for a single publisher domain.

    ``missing_in_inline``: property IDs the federated fetch found in the
    publisher's own adagents.json that the directory did not surface
    (publisher has properties the directory doesn't know about yet).

    ``missing_in_federated``: property IDs the directory claims the agent
    is authorized for but the publisher's own adagents.json does not
    include (stale directory entry or publisher revocation).

    Both fields are None in count-only fallback mode (directory did
    not return ``property_ids[]``). In count-only mode, count-equality
    does NOT guarantee set-equality — same-count substitutions are
    undetectable. Use ``?include=properties`` (adcp#4894) on directories
    that support it for full set-diff precision.

    ``child_fetch_error`` is non-None when the publisher's adagents.json
    could not be fetched or parsed; other fields carry no meaning.
    """

    publisher_domain: str
    directory_properties_authorized: int = Field(ge=0)
    federated_properties_found: int = Field(ge=0)
    missing_in_inline: list[str] | None = None
    missing_in_federated: list[str] | None = None
    child_fetch_error: str | None = None

Divergence record for a single publisher domain.

missing_in_inline: property IDs the federated fetch found in the publisher's own adagents.json that the directory did not surface (publisher has properties the directory doesn't know about yet).

missing_in_federated: property IDs the directory claims the agent is authorized for but the publisher's own adagents.json does not include (stale directory entry or publisher revocation).

Both fields are None in count-only fallback mode (directory did not return property_ids[]). In count-only mode, count-equality does NOT guarantee set-equality — same-count substitutions are undetectable. Use ?include=properties (adcp#4894) on directories that support it for full set-diff precision.

child_fetch_error is non-None when the publisher's adagents.json could not be fetched or parsed; other fields carry no meaning.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var child_fetch_error : str | None
var directory_properties_authorized : int
var federated_properties_found : int
var missing_in_federated : list[str] | None
var missing_in_inline : list[str] | None
var model_config
var publisher_domain : str

Inherited members

class PublisherPropertiesAll (**data: Any)
Expand source code
class PublisherPropertySelector1(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    publisher_domain: Annotated[
        str | None,
        Field(
            description="Domain where publisher's adagents.json is hosted (e.g., 'cnn.com'). XOR with `publisher_domains` — exactly one MUST be present on each `publisher_properties[]` entry; both-present and neither-present both fail validation.",
            pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$',
        ),
    ] = None
    publisher_domains: Annotated[
        list[PublisherDomain] | None,
        Field(
            description="Compact form for fanning the same selector across many publishers (e.g., a managed network listing every publisher it represents). Each entry is the domain where that publisher's adagents.json is hosted. Each listed domain MUST be canonicalized to lowercase (the `pattern` already rejects uppercase). Mutually exclusive with `publisher_domain`. Each listed domain counts as explicitly scoped for the `managerdomain` fallback safety rule.",
            min_length=1,
        ),
    ] = None
    selection_type: Annotated[
        Literal['all'],
        Field(
            description='Discriminator indicating all properties from each addressed publisher are included'
        ),
    ] = 'all'

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var model_config
var publisher_domain : str | None
var publisher_domains : list[adcp.types.generated_poc.core.publisher_property_selector.PublisherDomain] | None
var selection_type : Literal['all']

Inherited members

class PublisherPropertiesById (**data: Any)
Expand source code
class PublisherPropertySelector2(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    publisher_domain: Annotated[
        str,
        Field(
            description="Domain where publisher's adagents.json is hosted (e.g., 'cnn.com').",
            pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$',
        ),
    ]
    selection_type: Annotated[
        Literal['by_id'],
        Field(description='Discriminator indicating selection by specific property IDs'),
    ] = 'by_id'
    property_ids: Annotated[
        list[property_id.PropertyId],
        Field(description="Specific property IDs from the publisher's adagents.json", min_length=1),
    ]

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var model_config
var property_ids : list[adcp.types.generated_poc.core.property_id.PropertyId]
var publisher_domain : str
var selection_type : Literal['by_id']

Inherited members

class PublisherPropertiesByTag (**data: Any)
Expand source code
class PublisherPropertySelector3(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    publisher_domain: Annotated[
        str | None,
        Field(
            description="Domain where publisher's adagents.json is hosted (e.g., 'cnn.com'). XOR with `publisher_domains` — exactly one MUST be present on each `publisher_properties[]` entry; both-present and neither-present both fail validation.",
            pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$',
        ),
    ] = None
    publisher_domains: Annotated[
        list[PublisherDomain] | None,
        Field(
            description="Compact form for fanning the same tag predicate across many publishers (canonical managed-network shape). Each entry is the domain where that publisher's adagents.json is hosted. Each listed domain MUST be canonicalized to lowercase (the `pattern` already rejects uppercase). Mutually exclusive with `publisher_domain`. Each listed domain counts as explicitly scoped for the `managerdomain` fallback safety rule.",
            min_length=1,
        ),
    ] = None
    selection_type: Annotated[
        Literal['by_tag'], Field(description='Discriminator indicating selection by property tags')
    ] = 'by_tag'
    property_tags: Annotated[
        list[property_tag.PropertyTag],
        Field(
            description="Property tags resolved against each addressed publisher's adagents.json, OR against the parent file's top-level `properties[]` when those properties carry a `publisher_domain` matching the selector. Selector covers all properties carrying any of these tags.",
            min_length=1,
        ),
    ]

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var model_config
var property_tags : list[adcp.types.generated_poc.core.property_tag.PropertyTag]
var publisher_domain : str | None
var publisher_domains : list[adcp.types.generated_poc.core.publisher_property_selector.PublisherDomain] | None
var selection_type : Literal['by_tag']

Inherited members

class PushNotificationConfig (**data: Any)
Expand source code
class PushNotificationConfig(AdCPBaseModel):
    url: Annotated[
        AnyUrl,
        Field(
            description='Webhook endpoint URL for task status notifications. The wire contract is unconstrained beyond `format: "uri"` — in particular, publishers SHOULD NOT enforce a destination-port allowlist by default, since buyers legitimately host receivers on non-standard TLS ports (`:9443`, `:4443`, path-routed multi-tenant gateways). The SSRF guard the protocol relies on is the IP-range check + DNS-rebinding-resistant connect pin defined in [Webhook URL validation (SSRF)](/docs/building/by-layer/L1/security#webhook-url-validation-ssrf), not port filtering. Operators who want a hardened destination-port allowlist as defense-in-depth (e.g., locked-down enterprise egress) opt in explicitly — see [Destination port: permissive by default](/docs/building/by-layer/L1/security#destination-port-permissive-by-default).'
        ),
    ]
    operation_id: Annotated[
        str | None,
        Field(
            description="Buyer-supplied correlation identifier for the operation that will produce webhooks against this registration. The seller MUST echo this value verbatim into every webhook payload's `operation_id` field (see [`mcp-webhook-payload.json`](/schemas/core/mcp-webhook-payload.json) and [Webhooks — Operation IDs](/docs/building/by-layer/L3/webhooks#operation-ids-and-url-templates)). Buyers SHOULD generate a unique value per task invocation (UUID recommended). This field is the canonical registration channel for `operation_id`; buyers MAY additionally embed routing values in the URL path or query as an aid for their own HTTP server, but the URL is opaque to the seller and the wire-level source of truth is this field. Sellers MUST NOT parse the URL to recover `operation_id`. For 3.x schema compatibility the member remains optional, but a seller MUST reject a task that registers an AdCP webhook without it using `INVALID_REQUEST`; otherwise the required webhook envelope cannot be emitted.",
            max_length=255,
            min_length=1,
            pattern='^[A-Za-z0-9_.:-]{1,255}$',
        ),
    ] = None
    token: Annotated[
        str | None,
        Field(
            description="Optional client-provided token for webhook validation. The seller MUST echo this value verbatim in every webhook payload's `token` field (see [`mcp-webhook-payload.json`](/schemas/core/mcp-webhook-payload.json) for the receiver-side validation obligation). Length bounds give receivers a defensive range check on the echoed value; senders SHOULD generate tokens with at least 128 bits of entropy (≥22 base64url characters). This is a complementary authenticity mechanism that can layer on top of the RFC 9421 webhook signature — unlike the `authentication` block below, it is not on the 4.0 removal track. Receivers that registered both a signing key (RFC 9421) and a `token` MUST NOT treat a valid token echo as authorization to skip signature verification; both checks remain independent obligations.",
            max_length=4096,
            min_length=16,
        ),
    ] = None
    authentication: Annotated[
        Authentication | None,
        Field(
            deprecated=True,
            description='Legacy authentication configuration (A2A-compatible). Opts the seller into Bearer or HMAC-SHA256 signing instead of the default RFC 9421 webhook profile. Deprecated; removed in AdCP 4.0. **Precedence is a switch, not a fallback:** presence of this block selects the legacy scheme; absence selects 9421. A seller MUST NOT sign the same webhook both ways, and a buyer MUST NOT attempt \'try 9421 first, fall back to HMAC\' verification — signature mode is determined solely by whether this block was present at registration time. The seller\'s baseline 9421 webhook key is published at its brand.json `agents[]` `jwks_uri` using `adcp_use: "request-signing"` (deprecated `webhook-signing` keys remain accepted during the compatibility window); it does not override this selector and is only used when `authentication` is omitted. See docs/building/by-layer/L1/security.mdx#webhook-callbacks for the full precedence and downgrade-resistance rules (including the `webhook_mode_mismatch` rejection a buyer MUST apply when a received webhook\'s signing mode does not match the registered mode).',
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var authentication : adcp.types.generated_poc.core.push_notification_config.Authentication | None
var model_config
var operation_id : str | None
var token : str | None
var url : pydantic.networks.AnyUrl

Inherited members

class ReferenceRenderer (**data: Any)
Expand source code
class ReferenceRenderer(AdCPBaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    runtime: Annotated[
        Literal['browser-esm'],
        Field(
            description='Execution contract for the referenced package. browser-esm means a browser-safe ECMAScript module that accepts canonical manifest data and returns an inert presentation without Node.js APIs, ambient credentials, delivery tracking, or undeclared network access. Non-JavaScript clients use a hosted preview_creative provider or display the manifest.'
        ),
    ] = 'browser-esm'
    package: Annotated[
        str,
        Field(
            description='npm package name, scoped or unscoped. The package is resolved from the npm registry; the AdCP registry does not proxy its executable contents.',
            pattern='^(?:@[a-z0-9][a-z0-9._~-]*/)?[a-z0-9][a-z0-9._~-]*$',
        ),
    ]
    version: Annotated[
        str,
        Field(
            description='Exact semantic version. Ranges and tags such as latest are forbidden so the registry entry is reproducible. Package semantic versioning identifies the pinned distribution artifact; it is independent of any one format revision because one package may expose renderers for multiple formats.',
            pattern='^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$',
        ),
    ]
    export: Annotated[
        str,
        Field(
            description="Named package export that implements the renderer contract for this enclosing format entry. Compatibility is bound at the export-to-entry edge, not to the package major: the export's documented input/output contract MUST implement the enclosing format entry's revision. When that format entry moves to a new major revision, the registry MUST point it to a compatible export, rotating package version and integrity only when the selected artifact changes. One package version MAY expose different named exports for different formats or format revisions.",
            min_length=1,
        ),
    ]
    format_revision: Annotated[
        str,
        Field(
            description="Exact canonical format-entry revision implemented by this named export. It MUST equal the enclosing community format entry's format_revision; compatibility binds to this export edge, not the package major.",
            pattern='^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$',
        ),
    ]
    integrity: Annotated[
        str,
        Field(
            description='Subresource Integrity value for the exact npm package tarball. Consumers MUST compare this value before loading code, require the provenance subject digest to match the same tarball, and fail closed on mismatch.',
            pattern='^(?:sha256-[A-Za-z0-9+/]{43}=|sha384-[A-Za-z0-9+/]{64}|sha512-[A-Za-z0-9+/]{86}==)$',
        ),
    ]
    provenance: Provenance

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var export : str
var format_revision : str
var integrity : str
var model_config
var package : str
var provenance : adcp.types.generated_poc.core.reference_renderer.Provenance
var runtime : Literal['browser-esm']
var version : str

Inherited members

class Refine (root: RootModelRootType = PydanticUndefined, **data)
Expand source code
class Refine(RootModel[Refine1 | Refine2 | Refine3]):
    root: Annotated[Refine1 | Refine2 | Refine3, Field(discriminator='scope')]
    def __getattr__(self, name: str) -> Any:
        """Proxy attribute access to the wrapped type."""
        if name.startswith('_'):
            raise AttributeError(name)
        return getattr(self.root, name)

Usage Documentation

RootModel and Custom Root Types

A Pydantic BaseModel for the root object of the model.

Attributes
-----=
root
The root object of the model.
__pydantic_root_model__
Whether the model is a RootModel.
__pydantic_private__
Private fields in the model.
__pydantic_extra__
Extra fields in the model.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • pydantic.root_model.RootModel[Union[Refine1, Refine2, Refine3]]
  • pydantic.root_model.RootModel
  • pydantic.main.BaseModel
  • typing.Generic

Class variables

var model_config
var root : adcp.types.generated_poc.media_buy.get_products_request.Refine1 | adcp.types.generated_poc.media_buy.get_products_request.Refine2 | adcp.types.generated_poc.media_buy.get_products_request.Refine3
class RefineProposalsRequest (**data: Any)
Expand source code
class RefineProposalsRequest(AdCPBaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    adcp_version: version_envelope.AdcpVersion | None = None
    adcp_major_version: version_envelope.AdcpMajorVersion | None = None
    context_id: Annotated[str | None, Field(min_length=1)] = None
    context: context_1.ContextObject | None = None
    governance_context: Annotated[str | None, Field(max_length=4096, min_length=1)] = None
    push_notification_config: push_notification_config_1.PushNotificationConfig | None = None
    idempotency_key: Annotated[
        str,
        Field(
            description='Client-generated key required for retry-safe proposal refinement.',
            max_length=255,
            min_length=16,
            pattern='^[A-Za-z0-9_.:-]{16,255}$',
        ),
    ]
    refinements: Annotated[
        list[proposal_refinement.ProposalRefinement] | Refinements,
        Field(
            description='Proposal operations to apply, with at most 25 entries per request. revise creates a draft successor from a draft, committed, or accepted source. finalize MUST target a draft, reserves inventory, and creates a committed successor whose expires_at is the hold deadline. A batch containing finalize MUST contain only finalize entries and is atomic. proposal_id values MUST be unique; results preserve request order.',
            max_length=25,
            min_length=1,
        ),
    ]

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var adcp_major_version : adcp.types.generated_poc.core.version_envelope.AdcpMajorVersion | None
var adcp_version : adcp.types.generated_poc.core.version_envelope.AdcpVersion | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var context_id : str | None
var governance_context : str | None
var idempotency_key : str
var model_config
var push_notification_config : adcp.types.generated_poc.core.push_notification_config.PushNotificationConfig | None
var refinements : list[adcp.types.generated_poc.media_buy.proposal_refinement.ProposalRefinement] | adcp.types.generated_poc.media_buy.refine_proposals_request.Refinements

Inherited members

class RefreshResult (products_unchanged: bool = False,
signals_unchanged: bool = False,
product_count: int = 0,
signal_count: int = 0)
Expand source code
@dataclass
class RefreshResult:
    """Outcome of a :meth:`FeedMirror.refresh` / :meth:`bootstrap` call.

    ``unchanged`` is ``True`` when the seller short-circuited every requested
    feed with ``unchanged: true`` (the replica was not mutated).
    """

    products_unchanged: bool = False
    signals_unchanged: bool = False
    product_count: int = 0
    signal_count: int = 0

    @property
    def unchanged(self) -> bool:
        """True when no requested feed reported a change."""
        return self.products_unchanged and self.signals_unchanged

Outcome of a :meth:FeedMirror.refresh() / :meth:bootstrap call.

unchanged is True when the seller short-circuited every requested feed with unchanged: true (the replica was not mutated).

Instance variables

var product_count : int
var products_unchanged : bool
var signal_count : int
var signals_unchanged : bool
prop unchanged : bool
Expand source code
@property
def unchanged(self) -> bool:
    """True when no requested feed reported a change."""
    return self.products_unchanged and self.signals_unchanged

True when no requested feed reported a change.

class RegistryClient (base_url: str = 'https://agenticadvertising.org',
timeout: float = 10.0,
client: httpx.AsyncClient | None = None,
user_agent: str = 'adcp-client-python')
Expand source code
class RegistryClient:
    """Client for the AdCP registry API.

    Provides brand, property, and member lookups against the central AdCP registry.

    Args:
        base_url: Registry API base URL.
        timeout: Request timeout in seconds.
        client: Optional httpx.AsyncClient for connection pooling.
            If provided, caller is responsible for client lifecycle.
        user_agent: User-Agent header for requests.
    """

    def __init__(
        self,
        base_url: str = DEFAULT_REGISTRY_URL,
        timeout: float = 10.0,
        client: httpx.AsyncClient | None = None,
        user_agent: str = "adcp-client-python",
    ):
        self._base_url = base_url.rstrip("/")
        self._timeout = timeout
        self._external_client = client
        self._owned_client: httpx.AsyncClient | None = None
        self._user_agent = user_agent

    async def _get_client(self) -> httpx.AsyncClient:
        """Get or create httpx client."""
        if self._external_client is not None:
            return self._external_client
        if self._owned_client is None:
            self._owned_client = httpx.AsyncClient(
                limits=httpx.Limits(
                    max_keepalive_connections=10,
                    max_connections=20,
                ),
                trust_env=False,
            )
        return self._owned_client

    async def close(self) -> None:
        """Close owned HTTP client. No-op if using external client."""
        if self._owned_client is not None:
            await self._owned_client.aclose()
            self._owned_client = None

    async def __aenter__(self) -> RegistryClient:
        return self

    async def __aexit__(self, *args: Any) -> None:
        await self.close()

    async def _request(
        self,
        method: str,
        path: str,
        *,
        params: dict[str, Any] | None = None,
        json_body: dict[str, Any] | None = None,
        auth_token: str | None = None,
        operation: str = "Registry request",
        allow_404: bool = False,
        expected_status: int | set[int] = 200,
    ) -> httpx.Response | None:
        """Execute a registry API request with standard error handling.

        Returns None if allow_404=True and the server returns 404.
        Raises RegistryError for all other non-expected status codes.
        """
        client = await self._get_client()
        headers: dict[str, str] = {"User-Agent": self._user_agent}
        if auth_token is not None:
            headers["Authorization"] = f"Bearer {auth_token}"

        expected = {expected_status} if isinstance(expected_status, int) else expected_status

        try:
            url = f"{self._base_url}{path}"
            if method == "GET":
                response = await client.get(
                    url,
                    params=params,
                    headers=headers,
                    timeout=self._timeout,
                )
            elif method == "POST":
                response = await client.post(
                    url,
                    params=params,
                    json=json_body,
                    headers=headers,
                    timeout=self._timeout,
                )
            else:
                response = await client.request(
                    method,
                    url,
                    params=params,
                    json=json_body,
                    headers=headers,
                    timeout=self._timeout,
                )

            if allow_404 and response.status_code == 404:
                return None
            if response.status_code not in expected:
                raise _registry_http_error(
                    response,
                    method=method,
                    operation=operation,
                )
            return response
        except RegistryError:
            raise
        except httpx.TimeoutException as e:
            raise RegistryError(f"{operation} timed out after {self._timeout}s") from e
        except httpx.HTTPError as e:
            raise RegistryError(f"{operation} failed: {e}") from e

    async def _request_ok(
        self,
        method: str,
        path: str,
        **kwargs: Any,
    ) -> httpx.Response:
        """Like _request but guarantees a non-None response.

        Use for endpoints that never return 404-as-None.
        """
        resp = await self._request(method, path, **kwargs)
        if resp is None:
            raise RegistryError(
                f"{kwargs.get('operation', 'Request')} failed: unexpected empty response"
            )
        return resp

    async def _request_json(
        self,
        method: str,
        path: str,
        *,
        params: dict[str, Any] | None = None,
        json_body: dict[str, Any] | None = None,
        auth_token: str | None = None,
        operation: str = "Registry request",
        expected_status: int | set[int] = 200,
    ) -> dict[str, Any]:
        """Execute a registry request and return the JSON object body."""
        resp = await self._request_ok(
            method,
            path,
            params=params,
            json_body=json_body,
            auth_token=auth_token,
            operation=operation,
            expected_status=expected_status,
        )
        return cast(dict[str, Any], resp.json())

    async def _request_text(
        self,
        method: str,
        path: str,
        *,
        params: dict[str, Any] | None = None,
        json_body: dict[str, Any] | None = None,
        auth_token: str | None = None,
        operation: str = "Registry request",
        expected_status: int | set[int] = 200,
    ) -> str:
        """Execute a registry request and return the text body."""
        resp = await self._request_ok(
            method,
            path,
            params=params,
            json_body=json_body,
            auth_token=auth_token,
            operation=operation,
            expected_status=expected_status,
        )
        return resp.text

    @staticmethod
    def _parse(model_cls: type[_T], data: Any, operation: str) -> _T:
        """Validate data against a Pydantic model, wrapping errors."""
        try:
            return model_cls.model_validate(data)
        except (ValidationError, ValueError) as e:
            raise RegistryError(f"{operation} failed: invalid response: {e}") from e

    async def lookup_brand(self, domain: str, *, fresh: bool = False) -> ResolvedBrand | None:
        """Resolve a domain to its brand identity.

        Works for any domain — brand houses, sub-brands, and operators
        (agencies, DSPs) are all brands in the registry.

        Args:
            domain: Domain to resolve (e.g., "nike.com", "wpp.com").
            fresh: Request a live origin check instead of a cached registry
                result. Defaults to False. Use for authorization decisions
                that require current brand relationship evidence.

        Returns:
            ResolvedBrand if found, None if not in the registry.

        Raises:
            RegistryError: On HTTP or parsing errors.

        Example:
            brand = await registry.lookup_brand(request.brand.domain)
        """
        try:
            params = {"domain": domain}
            if fresh:
                params["fresh"] = "true"
            response = await self._request(
                "GET",
                "/api/brands/resolve",
                params=params,
                operation="Brand lookup",
                allow_404=True,
            )
            if response is None:
                return None
            data = response.json()
            if data is None:
                return None
            return ResolvedBrand.model_validate(data)
        except RegistryError:
            raise
        except httpx.TimeoutException as e:
            raise RegistryError(f"Brand lookup timed out after {self._timeout}s") from e
        except httpx.HTTPError as e:
            raise RegistryError(f"Brand lookup failed: {e}") from e
        except (ValidationError, ValueError) as e:
            raise RegistryError(f"Brand lookup failed: invalid response: {e}") from e

    async def lookup_brands(self, domains: list[str]) -> dict[str, ResolvedBrand | None]:
        """Bulk resolve domains to brand identities.

        Automatically chunks requests exceeding 100 domains.

        Args:
            domains: List of domains to resolve.

        Returns:
            Dict mapping each domain to its ResolvedBrand, or None if not found.

        Raises:
            RegistryError: On HTTP or parsing errors.
        """
        if not domains:
            return {}

        chunks = [
            domains[i : i + MAX_BULK_DOMAINS] for i in range(0, len(domains), MAX_BULK_DOMAINS)
        ]

        chunk_results = await asyncio.gather(
            *[self._lookup_brands_chunk(chunk) for chunk in chunks]
        )

        merged: dict[str, ResolvedBrand | None] = {}
        for result in chunk_results:
            merged.update(result)
        return merged

    async def _lookup_brands_chunk(self, domains: list[str]) -> dict[str, ResolvedBrand | None]:
        """Resolve a single chunk of brand domains (max 100)."""
        client = await self._get_client()
        try:
            response = await client.post(
                f"{self._base_url}/api/brands/resolve/bulk",
                json={"domains": domains},
                headers={"User-Agent": self._user_agent},
                timeout=self._timeout,
            )
            if response.status_code != 200:
                raise _registry_http_error(
                    response,
                    method="POST",
                    operation="Bulk brand lookup",
                )
            data = response.json()
            results_raw = data.get("results", {})
            results: dict[str, ResolvedBrand | None] = {d: None for d in domains}
            for domain, brand_data in results_raw.items():
                if brand_data is not None:
                    results[domain] = ResolvedBrand.model_validate(brand_data)
            return results
        except RegistryError:
            raise
        except httpx.TimeoutException as e:
            raise RegistryError(f"Bulk brand lookup timed out after {self._timeout}s") from e
        except httpx.HTTPError as e:
            raise RegistryError(f"Bulk brand lookup failed: {e}") from e
        except (ValidationError, ValueError) as e:
            raise RegistryError(f"Bulk brand lookup failed: invalid response: {e}") from e

    async def lookup_property(self, domain: str) -> ResolvedProperty | None:
        """Resolve a publisher domain to its property info.

        Args:
            domain: Publisher domain to resolve (e.g., "nytimes.com").

        Returns:
            ResolvedProperty if found, None if the domain is not in the registry.

        Raises:
            RegistryError: On HTTP or parsing errors.
        """
        client = await self._get_client()
        try:
            response = await client.get(
                f"{self._base_url}/api/properties/resolve",
                params={"domain": domain},
                headers={"User-Agent": self._user_agent},
                timeout=self._timeout,
            )
            if response.status_code == 404:
                return None
            if response.status_code != 200:
                raise _registry_http_error(
                    response,
                    method="GET",
                    operation="Property lookup",
                )
            data = response.json()
            if data is None:
                return None
            return ResolvedProperty.model_validate(data)
        except RegistryError:
            raise
        except httpx.TimeoutException as e:
            raise RegistryError(f"Property lookup timed out after {self._timeout}s") from e
        except httpx.HTTPError as e:
            raise RegistryError(f"Property lookup failed: {e}") from e
        except (ValidationError, ValueError) as e:
            raise RegistryError(f"Property lookup failed: invalid response: {e}") from e

    async def lookup_properties(self, domains: list[str]) -> dict[str, ResolvedProperty | None]:
        """Bulk resolve publisher domains to property info.

        Automatically chunks requests exceeding 100 domains.

        Args:
            domains: List of publisher domains to resolve.

        Returns:
            Dict mapping each domain to its ResolvedProperty, or None if not found.

        Raises:
            RegistryError: On HTTP or parsing errors.
        """
        if not domains:
            return {}

        chunks = [
            domains[i : i + MAX_BULK_DOMAINS] for i in range(0, len(domains), MAX_BULK_DOMAINS)
        ]

        chunk_results = await asyncio.gather(
            *[self._lookup_properties_chunk(chunk) for chunk in chunks]
        )

        merged: dict[str, ResolvedProperty | None] = {}
        for result in chunk_results:
            merged.update(result)
        return merged

    async def _lookup_properties_chunk(
        self, domains: list[str]
    ) -> dict[str, ResolvedProperty | None]:
        """Resolve a single chunk of property domains (max 100)."""
        client = await self._get_client()
        try:
            response = await client.post(
                f"{self._base_url}/api/properties/resolve/bulk",
                json={"domains": domains},
                headers={"User-Agent": self._user_agent},
                timeout=self._timeout,
            )
            if response.status_code != 200:
                raise _registry_http_error(
                    response,
                    method="POST",
                    operation="Bulk property lookup",
                )
            data = response.json()
            results_raw = data.get("results", {})
            results: dict[str, ResolvedProperty | None] = {d: None for d in domains}
            for domain, prop_data in results_raw.items():
                if prop_data is not None:
                    results[domain] = ResolvedProperty.model_validate(prop_data)
            return results
        except RegistryError:
            raise
        except httpx.TimeoutException as e:
            raise RegistryError(f"Bulk property lookup timed out after {self._timeout}s") from e
        except httpx.HTTPError as e:
            raise RegistryError(f"Bulk property lookup failed: {e}") from e
        except (ValidationError, ValueError) as e:
            raise RegistryError(f"Bulk property lookup failed: invalid response: {e}") from e

    async def list_members(self, limit: int = 100) -> list[Member]:
        """List organizations registered in the AAO member directory.

        Args:
            limit: Maximum number of members to return.

        Returns:
            List of Member objects.

        Raises:
            RegistryError: On HTTP or parsing errors.
        """
        if limit < 1:
            raise ValueError(f"limit must be at least 1, got {limit}")

        client = await self._get_client()
        try:
            response = await client.get(
                f"{self._base_url}/api/members",
                params={"limit": limit},
                headers={"User-Agent": self._user_agent},
                timeout=self._timeout,
            )
            if response.status_code != 200:
                raise _registry_http_error(
                    response,
                    method="GET",
                    operation="Member list",
                )
            data = response.json()
            return [Member.model_validate(m) for m in data.get("members", [])]
        except RegistryError:
            raise
        except httpx.TimeoutException as e:
            raise RegistryError(f"Member list timed out after {self._timeout}s") from e
        except httpx.HTTPError as e:
            raise RegistryError(f"Member list failed: {e}") from e
        except (ValidationError, ValueError) as e:
            raise RegistryError(f"Member list failed: invalid response: {e}") from e

    async def get_member(self, slug: str) -> Member | None:
        """Get a single AAO member by their slug.

        Args:
            slug: Member slug (e.g., "adgentek").

        Returns:
            Member if found, None if not in the registry.

        Raises:
            RegistryError: On HTTP or parsing errors.
            ValueError: If slug contains path-traversal characters.
        """
        if not slug or not re.fullmatch(r"[a-zA-Z0-9_-]+", slug):
            raise ValueError(f"Invalid member slug: {slug!r}")
        client = await self._get_client()
        try:
            response = await client.get(
                f"{self._base_url}/api/members/{slug}",
                headers={"User-Agent": self._user_agent},
                timeout=self._timeout,
            )
            if response.status_code == 404:
                return None
            if response.status_code != 200:
                raise _registry_http_error(
                    response,
                    method="GET",
                    operation="Member lookup",
                )
            data = response.json()
            if data is None:
                return None
            return Member.model_validate(data)
        except RegistryError:
            raise
        except httpx.TimeoutException as e:
            raise RegistryError(f"Member lookup timed out after {self._timeout}s") from e
        except httpx.HTTPError as e:
            raise RegistryError(f"Member lookup failed: {e}") from e
        except (ValidationError, ValueError) as e:
            raise RegistryError(f"Member lookup failed: invalid response: {e}") from e

    # ========================================================================
    # Policy Registry Operations
    # ========================================================================

    async def list_policies(
        self,
        search: str | None = None,
        category: str | None = None,
        enforcement: str | None = None,
        jurisdiction: str | None = None,
        vertical: str | None = None,
        domain: str | None = None,
        limit: int = 20,
        offset: int = 0,
    ) -> list[PolicySummary]:
        """List governance policies with optional filtering.

        Args:
            search: Full-text search on policy name and description.
            category: Filter by category ("regulation" or "standard").
            enforcement: Filter by enforcement level ("must", "should", "may").
            jurisdiction: Filter by jurisdiction with region alias matching.
            vertical: Filter by industry vertical.
            domain: Filter by governance domain ("campaign", "creative", etc.).
            limit: Results per page (default 20, max 1000).
            offset: Pagination offset.

        Returns:
            List of PolicySummary objects.

        Raises:
            RegistryError: On HTTP or parsing errors.
        """
        client = await self._get_client()
        params: dict[str, str | int] = {"limit": limit, "offset": offset}
        if search is not None:
            params["search"] = search
        if category is not None:
            params["category"] = category
        if enforcement is not None:
            params["enforcement"] = enforcement
        if jurisdiction is not None:
            params["jurisdiction"] = jurisdiction
        if vertical is not None:
            params["vertical"] = vertical
        if domain is not None:
            params["domain"] = domain

        try:
            response = await client.get(
                f"{self._base_url}/api/policies/registry",
                params=params,
                headers={"User-Agent": self._user_agent},
                timeout=self._timeout,
            )
            if response.status_code != 200:
                raise _registry_http_error(
                    response,
                    method="GET",
                    operation="Policy list",
                )
            data = response.json()
            return [PolicySummary.model_validate(p) for p in data.get("policies", [])]
        except RegistryError:
            raise
        except httpx.TimeoutException as e:
            raise RegistryError(f"Policy list timed out after {self._timeout}s") from e
        except httpx.HTTPError as e:
            raise RegistryError(f"Policy list failed: {e}") from e
        except (ValidationError, ValueError) as e:
            raise RegistryError(f"Policy list failed: invalid response: {e}") from e

    async def resolve_policy(
        self,
        policy_id: str,
        version: str | None = None,
    ) -> Policy | None:
        """Resolve a single policy by ID.

        Args:
            policy_id: Policy identifier (e.g., "gdpr_consent").
            version: Optional version pin; returns None if current version differs.

        Returns:
            Policy if found, None if not in the registry.

        Raises:
            RegistryError: On HTTP or parsing errors.
        """
        client = await self._get_client()
        params: dict[str, str] = {"policy_id": policy_id}
        if version is not None:
            params["version"] = version

        try:
            response = await client.get(
                f"{self._base_url}/api/policies/resolve",
                params=params,
                headers={"User-Agent": self._user_agent},
                timeout=self._timeout,
            )
            if response.status_code == 404:
                return None
            if response.status_code != 200:
                raise _registry_http_error(
                    response,
                    method="GET",
                    operation="Policy resolve",
                )
            data = response.json()
            if data is None:
                return None
            return Policy.model_validate(data)
        except RegistryError:
            raise
        except httpx.TimeoutException as e:
            raise RegistryError(f"Policy resolve timed out after {self._timeout}s") from e
        except httpx.HTTPError as e:
            raise RegistryError(f"Policy resolve failed: {e}") from e
        except (ValidationError, ValueError) as e:
            raise RegistryError(f"Policy resolve failed: invalid response: {e}") from e

    async def resolve_policies(
        self,
        policy_ids: list[str],
    ) -> dict[str, Policy | None]:
        """Bulk resolve policies by ID.

        Automatically chunks requests exceeding 100 policy IDs.

        Args:
            policy_ids: List of policy identifiers to resolve.

        Returns:
            Dict mapping each policy_id to its Policy, or None if not found.

        Raises:
            RegistryError: On HTTP or parsing errors.
        """
        if not policy_ids:
            return {}

        chunks = [
            policy_ids[i : i + MAX_BULK_POLICIES]
            for i in range(0, len(policy_ids), MAX_BULK_POLICIES)
        ]

        chunk_results = await asyncio.gather(
            *[self._resolve_policies_chunk(chunk) for chunk in chunks]
        )

        merged: dict[str, Policy | None] = {}
        for result in chunk_results:
            merged.update(result)
        return merged

    async def _resolve_policies_chunk(self, policy_ids: list[str]) -> dict[str, Policy | None]:
        """Resolve a single chunk of policy IDs (max 100)."""
        client = await self._get_client()
        try:
            response = await client.post(
                f"{self._base_url}/api/policies/resolve/bulk",
                json={"policy_ids": policy_ids},
                headers={"User-Agent": self._user_agent},
                timeout=self._timeout,
            )
            if response.status_code != 200:
                raise _registry_http_error(
                    response,
                    method="POST",
                    operation="Bulk policy resolve",
                )
            data = response.json()
            results_raw = data.get("results", {})
            results: dict[str, Policy | None] = {pid: None for pid in policy_ids}
            for pid, policy_data in results_raw.items():
                if policy_data is not None:
                    results[pid] = Policy.model_validate(policy_data)
            return results
        except RegistryError:
            raise
        except httpx.TimeoutException as e:
            raise RegistryError(f"Bulk policy resolve timed out after {self._timeout}s") from e
        except httpx.HTTPError as e:
            raise RegistryError(f"Bulk policy resolve failed: {e}") from e
        except (ValidationError, ValueError) as e:
            raise RegistryError(f"Bulk policy resolve failed: invalid response: {e}") from e

    async def policy_history(
        self,
        policy_id: str,
        limit: int = 20,
        offset: int = 0,
    ) -> PolicyHistory | None:
        """Retrieve edit history for a policy.

        Args:
            policy_id: Policy identifier.
            limit: Maximum revisions to return (default 20, max 100).
            offset: Pagination offset.

        Returns:
            PolicyHistory if found, None if the policy doesn't exist.

        Raises:
            RegistryError: On HTTP or parsing errors.
        """
        client = await self._get_client()
        try:
            response = await client.get(
                f"{self._base_url}/api/policies/history",
                params={"policy_id": policy_id, "limit": limit, "offset": offset},
                headers={"User-Agent": self._user_agent},
                timeout=self._timeout,
            )
            if response.status_code == 404:
                return None
            if response.status_code != 200:
                raise _registry_http_error(
                    response,
                    method="GET",
                    operation="Policy history",
                )
            data = response.json()
            if data is None:
                return None
            return PolicyHistory.model_validate(data)
        except RegistryError:
            raise
        except httpx.TimeoutException as e:
            raise RegistryError(f"Policy history timed out after {self._timeout}s") from e
        except httpx.HTTPError as e:
            raise RegistryError(f"Policy history failed: {e}") from e
        except (ValidationError, ValueError) as e:
            raise RegistryError(f"Policy history failed: invalid response: {e}") from e

    async def save_policy(
        self,
        policy_id: str,
        version: str,
        name: str,
        category: str,
        enforcement: str,
        policy: str,
        *,
        auth_token: str,
        description: str | None = None,
        jurisdictions: list[str] | None = None,
        region_aliases: dict[str, list[str]] | None = None,
        verticals: list[str] | None = None,
        channels: list[str] | None = None,
        effective_date: str | None = None,
        sunset_date: str | None = None,
        governance_domains: list[str] | None = None,
        source_url: str | None = None,
        source_name: str | None = None,
        guidance: str | None = None,
        exemplars: dict[str, Any] | None = None,
        ext: dict[str, Any] | None = None,
    ) -> dict[str, Any]:
        """Create or update a community-contributed policy.

        Requires authentication. Cannot edit registry-sourced or pending policies.

        Args:
            policy_id: Policy identifier (lowercase alphanumeric with underscores).
            version: Semantic version string.
            name: Human-readable policy name.
            category: "regulation" or "standard".
            enforcement: "must", "should", or "may".
            policy: Natural language policy text.
            auth_token: API key for authentication.
            description: Policy description.
            jurisdictions: ISO jurisdiction codes.
            region_aliases: Region alias mappings (e.g., {"EU": ["DE", "FR"]}).
            verticals: Industry verticals.
            channels: Media channels.
            effective_date: ISO 8601 date when enforcement begins.
            sunset_date: ISO 8601 date when enforcement ends.
            governance_domains: Applicable domains ("campaign", "creative", etc.).
            source_url: URL of the source regulation/standard.
            source_name: Name of the source.
            guidance: Implementation guidance text.
            exemplars: Pass/fail calibration scenarios.
            ext: Extension data.

        Returns:
            Dict with success, message, policy_id, and revision_number.

        Raises:
            RegistryError: On HTTP or parsing errors (400, 401, 409, 429).
        """
        client = await self._get_client()
        body: dict[str, Any] = {
            "policy_id": policy_id,
            "version": version,
            "name": name,
            "category": category,
            "enforcement": enforcement,
            "policy": policy,
        }
        for key, value in [
            ("description", description),
            ("jurisdictions", jurisdictions),
            ("region_aliases", region_aliases),
            ("verticals", verticals),
            ("channels", channels),
            ("effective_date", effective_date),
            ("sunset_date", sunset_date),
            ("governance_domains", governance_domains),
            ("source_url", source_url),
            ("source_name", source_name),
            ("guidance", guidance),
            ("exemplars", exemplars),
            ("ext", ext),
        ]:
            if value is not None:
                body[key] = value

        try:
            response = await client.post(
                f"{self._base_url}/api/policies/save",
                json=body,
                headers={
                    "User-Agent": self._user_agent,
                    "Authorization": f"Bearer {auth_token}",
                },
                timeout=self._timeout,
            )
            if response.status_code != 200:
                raise _registry_http_error(
                    response,
                    method="POST",
                    operation="Policy save",
                )
            result: dict[str, Any] = response.json()
            return result
        except RegistryError:
            raise
        except httpx.TimeoutException as e:
            raise RegistryError(f"Policy save timed out after {self._timeout}s") from e
        except httpx.HTTPError as e:
            raise RegistryError(f"Policy save failed: {e}") from e

    # ========================================================================
    # Brand Registry Operations
    # ========================================================================

    async def get_brand_json(self, domain: str, *, fresh: bool = False) -> dict[str, Any] | None:
        """Fetch raw brand.json for a domain."""
        params: dict[str, Any] = {"domain": domain}
        if fresh:
            params["fresh"] = "true"
        resp = await self._request(
            "GET",
            "/api/brands/brand-json",
            params=params,
            allow_404=True,
            operation="Brand JSON fetch",
        )
        if resp is None:
            return None
        return cast(dict[str, Any], resp.json())

    async def save_brand(
        self,
        domain: str,
        brand_name: str,
        *,
        auth_token: str,
        brand_manifest: dict[str, Any] | None = None,
    ) -> dict[str, Any]:
        """Save or update a brand in the registry (auth required)."""
        body: dict[str, Any] = {"domain": domain, "brand_name": brand_name}
        if brand_manifest is not None:
            body["brand_manifest"] = brand_manifest
        resp = await self._request_ok(
            "POST",
            "/api/brands/save",
            json_body=body,
            auth_token=auth_token,
            operation="Brand save",
        )

        return cast(dict[str, Any], resp.json())

    async def list_brands(
        self,
        search: str | None = None,
        limit: int = 100,
        offset: int = 0,
    ) -> list[BrandRegistryItem]:
        """List brands in the registry."""
        params: dict[str, Any] = {"limit": limit, "offset": offset}
        if search is not None:
            params["search"] = search
        resp = await self._request_ok(
            "GET",
            "/api/brands/registry",
            params=params,
            operation="Brand list",
        )

        data = resp.json()
        return [self._parse(BrandRegistryItem, b, "Brand list") for b in data.get("brands", [])]

    async def brand_history(
        self,
        domain: str,
        limit: int = 20,
        offset: int = 0,
    ) -> BrandActivity | None:
        """Get edit history for a brand."""
        resp = await self._request(
            "GET",
            "/api/brands/history",
            params={"domain": domain, "limit": limit, "offset": offset},
            allow_404=True,
            operation="Brand history",
        )
        if resp is None:
            return None
        return self._parse(BrandActivity, resp.json(), "Brand history")

    async def enrich_brand(self, domain: str) -> dict[str, Any]:
        """Enrich brand data using Brandfetch."""
        resp = await self._request_ok(
            "GET",
            "/api/brands/enrich",
            params={"domain": domain},
            operation="Brand enrich",
        )

        return cast(dict[str, Any], resp.json())

    # ========================================================================
    # Property Registry Operations
    # ========================================================================

    async def list_properties(
        self,
        search: str | None = None,
        limit: int = 100,
        offset: int = 0,
    ) -> list[PropertyRegistryItem]:
        """List properties in the registry."""
        params: dict[str, Any] = {"limit": limit, "offset": offset}
        if search is not None:
            params["search"] = search
        resp = await self._request_ok(
            "GET",
            "/api/properties/registry",
            params=params,
            operation="Property list",
        )

        data = resp.json()
        return [
            self._parse(PropertyRegistryItem, p, "Property list")
            for p in data.get("properties", [])
        ]

    async def validate_property(self, domain: str) -> ValidationResult:
        """Validate a domain's adagents.json configuration."""
        resp = await self._request_ok(
            "GET",
            "/api/properties/validate",
            params={"domain": domain},
            operation="Property validate",
        )

        return self._parse(ValidationResult, resp.json(), "Property validate")

    async def save_property(
        self,
        publisher_domain: str,
        authorized_agents: list[dict[str, Any]],
        *,
        auth_token: str,
        properties: list[dict[str, Any]] | None = None,
        contact: dict[str, str] | None = None,
    ) -> dict[str, Any]:
        """Save or update a hosted property (auth required)."""
        body: dict[str, Any] = {
            "publisher_domain": publisher_domain,
            "authorized_agents": authorized_agents,
        }
        if properties is not None:
            body["properties"] = properties
        if contact is not None:
            body["contact"] = contact
        resp = await self._request_ok(
            "POST",
            "/api/properties/save",
            json_body=body,
            auth_token=auth_token,
            operation="Property save",
        )

        return cast(dict[str, Any], resp.json())

    async def property_history(
        self,
        domain: str,
        limit: int = 20,
        offset: int = 0,
    ) -> PropertyActivity | None:
        """Get edit history for a property."""
        resp = await self._request(
            "GET",
            "/api/properties/history",
            params={"domain": domain, "limit": limit, "offset": offset},
            allow_404=True,
            operation="Property history",
        )
        if resp is None:
            return None
        return self._parse(PropertyActivity, resp.json(), "Property history")

    async def check_property_list(self, domains: list[str]) -> dict[str, Any]:
        """Check publisher domains against the registry."""
        resp = await self._request_ok(
            "POST",
            "/api/properties/check",
            json_body={"domains": domains},
            operation="Property check",
        )

        return cast(dict[str, Any], resp.json())

    async def get_property_check_report(self, report_id: str) -> dict[str, Any] | None:
        """Retrieve a property check report by ID."""
        resp = await self._request(
            "GET",
            f"/api/properties/check/{url_quote(report_id, safe='')}",
            allow_404=True,
            operation="Property check report",
        )
        if resp is None:
            return None
        return cast(dict[str, Any], resp.json())

    async def verify_hosted_property_origin(
        self,
        domain: str,
        *,
        auth_token: str | None = None,
    ) -> dict[str, Any]:
        """Verify a hosted property's origin adagents.json delegation."""
        return await self._request_json(
            "POST",
            f"/api/properties/hosted/{url_quote(domain, safe='')}/verify-origin",
            auth_token=auth_token,
            operation="Hosted property origin verification",
        )

    # ========================================================================
    # Agent Discovery
    # ========================================================================

    async def list_agents(
        self,
        *,
        type: str | None = None,
        health: bool = False,
        capabilities: bool = False,
        properties: bool = False,
        compliance: bool = False,
        metric_id: str | list[str] | None = None,
        accreditation: str | list[str] | None = None,
        q: str | None = None,
        verification_mode: str | list[str] | None = None,
        verified: bool = False,
    ) -> list[FederatedAgentWithDetails]:
        """List registered and discovered agents.

        Measurement filters (``metric_id``, ``accreditation``, ``q``) imply
        ``type=measurement`` on the registry when ``type`` is omitted.
        """
        params: dict[str, Any] = {}
        if type is not None:
            params["type"] = type
        if health:
            params["health"] = "true"
        if capabilities:
            params["capabilities"] = "true"
        if properties:
            params["properties"] = "true"
        if compliance:
            params["compliance"] = "true"
        if metric_id is not None:
            params["metric_id"] = metric_id
        if accreditation is not None:
            params["accreditation"] = accreditation
        if q is not None:
            params["q"] = q
        if verification_mode is not None:
            params["verification_mode"] = verification_mode
        if verified:
            params["verified"] = "true"
        resp = await self._request_ok(
            "GET",
            "/api/registry/agents",
            params=params,
            operation="Agent list",
        )

        data = resp.json()
        return [
            self._parse(FederatedAgentWithDetails, a, "Agent list") for a in data.get("agents", [])
        ]

    async def list_publishers(self) -> list[FederatedPublisher]:
        """List publishers in the registry."""
        resp = await self._request_ok(
            "GET",
            "/api/registry/publishers",
            operation="Publisher list",
        )

        data = resp.json()
        return [
            self._parse(FederatedPublisher, p, "Publisher list") for p in data.get("publishers", [])
        ]

    async def get_registry_stats(self) -> dict[str, Any]:
        """Get aggregate registry statistics."""
        resp = await self._request_ok(
            "GET",
            "/api/registry/stats",
            operation="Registry stats",
        )

        return cast(dict[str, Any], resp.json())

    async def search_agents(
        self,
        *,
        auth_token: str,
        channels: str | None = None,
        property_types: str | None = None,
        markets: str | None = None,
        categories: str | None = None,
        tags: str | None = None,
        delivery_types: str | None = None,
        has_tmp: bool | None = None,
        min_properties: int | None = None,
        cursor: str | None = None,
        limit: int = 50,
    ) -> dict[str, Any]:
        """Search agents by inventory profile (auth required)."""
        params: dict[str, Any] = {"limit": limit}
        for key, val in [
            ("channels", channels),
            ("property_types", property_types),
            ("markets", markets),
            ("categories", categories),
            ("tags", tags),
            ("delivery_types", delivery_types),
            ("cursor", cursor),
        ]:
            if val is not None:
                params[key] = val
        if has_tmp is not None:
            params["has_tmp"] = str(has_tmp).lower()
        if min_properties is not None:
            params["min_properties"] = min_properties
        resp = await self._request_ok(
            "GET",
            "/api/registry/agents/search",
            params=params,
            auth_token=auth_token,
            operation="Agent search",
        )

        return cast(dict[str, Any], resp.json())

    async def request_crawl(self, domain: str, *, auth_token: str) -> dict[str, Any]:
        """Request a domain re-crawl (auth required)."""
        resp = await self._request_ok(
            "POST",
            "/api/registry/crawl-request",
            json_body={"domain": domain},
            auth_token=auth_token,
            operation="Crawl request",
            expected_status={200, 202},
        )

        return cast(dict[str, Any], resp.json())

    async def request_manager_revalidation(
        self,
        *,
        auth_token: str,
        **body: Any,
    ) -> dict[str, Any]:
        """Request manager revalidation for registry-managed data."""
        return await self._request_json(
            "POST",
            "/api/registry/manager-revalidation-request",
            json_body=dict(body),
            auth_token=auth_token,
            operation="Manager revalidation request",
            expected_status={200, 202},
        )

    async def request_brand_crawl(
        self,
        *,
        auth_token: str,
        **body: Any,
    ) -> dict[str, Any]:
        """Request a brand crawl through the registry."""
        return await self._request_json(
            "POST",
            "/api/registry/brand-crawl-request",
            json_body=dict(body),
            auth_token=auth_token,
            operation="Brand crawl request",
            expected_status={200, 202},
        )

    # ========================================================================
    # Lookups & Authorization
    # ========================================================================

    async def lookup_domain(self, domain: str) -> DomainLookupResult:
        """Find all agents authorized for a publisher domain."""
        resp = await self._request_ok(
            "GET",
            f"/api/registry/lookup/domain/{url_quote(domain, safe='')}",
            operation="Domain lookup",
        )

        return self._parse(DomainLookupResult, resp.json(), "Domain lookup")

    async def lookup_property_identifier(self, type: str, value: str) -> dict[str, Any]:
        """Find agents holding a specific property identifier."""
        resp = await self._request_ok(
            "GET",
            "/api/registry/lookup/property",
            params={"type": type, "value": value},
            operation="Property identifier lookup",
        )

        return cast(dict[str, Any], resp.json())

    async def get_agent_domains(self, agent_url: str) -> dict[str, Any]:
        """Get all publisher domains and identifiers for an agent."""
        encoded = url_quote(agent_url, safe="")
        resp = await self._request_ok(
            "GET",
            f"/api/registry/lookup/agent/{encoded}/domains",
            operation="Agent domains lookup",
        )

        return cast(dict[str, Any], resp.json())

    async def get_publishers_for_agent(
        self,
        agent_url: str,
        *,
        since: str | None = None,
        cursor: str | None = None,
        status: str | None = None,
        include: str | None = None,
        limit: int | None = None,
        legacy_api_prefix: bool = False,
    ) -> dict[str, Any]:
        """List publishers associated with an agent URL."""
        encoded = url_quote(agent_url, safe="")
        params = {
            k: v
            for k, v in {
                "since": since,
                "cursor": cursor,
                "status": status,
                "include": include,
                "limit": limit,
            }.items()
            if v is not None
        }
        prefix = "/api/v1" if legacy_api_prefix else "/v1"
        return await self._request_json(
            "GET",
            f"{prefix}/agents/{encoded}/publishers",
            params=params,
            operation="Publishers for agent",
        )

    async def lookup_operator(
        self,
        domain: str,
        *,
        scope: str | None = None,
    ) -> dict[str, Any]:
        """Resolve registry operator metadata for a domain."""
        params: dict[str, Any] = {"domain": domain}
        if scope is not None:
            params["scope"] = scope
        return await self._request_json(
            "GET",
            "/api/registry/operator",
            params=params,
            operation="Operator lookup",
        )

    async def lookup_publisher(self, domain: str) -> dict[str, Any]:
        """Resolve registry publisher metadata for a domain."""
        return await self._request_json(
            "GET",
            "/api/registry/publisher",
            params={"domain": domain},
            operation="Publisher lookup",
        )

    async def lookup_publisher_agent_authorization(
        self,
        domain: str,
        agent: str,
    ) -> dict[str, Any]:
        """Resolve whether a publisher authorizes an agent."""
        return await self._request_json(
            "GET",
            "/api/registry/publisher/authorization",
            params={"domain": domain, "agent": agent},
            operation="Publisher agent authorization lookup",
        )

    async def get_agent_authorizations(
        self,
        agent_url: str,
        *,
        auth_token: str | None = None,
        include: str | None = None,
        evidence: str | None = None,
    ) -> dict[str, Any]:
        """Fetch authorization rows for one agent."""
        params = {"agent_url": agent_url}
        if include is not None:
            params["include"] = include
        if evidence is not None:
            params["evidence"] = evidence
        return await self._request_json(
            "GET",
            "/api/registry/authorizations",
            params=params,
            auth_token=auth_token,
            operation="Agent authorizations",
        )

    async def get_agent_authorizations_snapshot(
        self,
        *,
        auth_token: str | None = None,
        include: str | None = None,
        evidence: str | None = None,
    ) -> dict[str, Any]:
        """Fetch the full authorization snapshot metadata."""
        params = {k: v for k, v in {"include": include, "evidence": evidence}.items() if v}
        return await self._request_json(
            "GET",
            "/api/registry/authorizations/snapshot",
            params=params,
            auth_token=auth_token,
            operation="Agent authorizations snapshot",
        )

    async def validate_product_authorization(
        self,
        agent_url: str,
        publisher_properties: list[dict[str, Any]],
    ) -> dict[str, Any]:
        """Check whether an agent is authorized to sell products."""
        resp = await self._request_ok(
            "POST",
            "/api/registry/validate/product-authorization",
            json_body={
                "agent_url": agent_url,
                "publisher_properties": publisher_properties,
            },
            operation="Product authorization",
        )

        return cast(dict[str, Any], resp.json())

    async def expand_product_identifiers(
        self,
        agent_url: str,
        publisher_properties: list[dict[str, Any]],
    ) -> dict[str, Any]:
        """Expand publisher_properties selectors into concrete identifiers."""
        resp = await self._request_ok(
            "POST",
            "/api/registry/expand/product-identifiers",
            json_body={
                "agent_url": agent_url,
                "publisher_properties": publisher_properties,
            },
            operation="Expand product identifiers",
        )

        return cast(dict[str, Any], resp.json())

    async def validate_property_authorization(
        self,
        agent_url: str,
        identifier_type: str,
        identifier_value: str,
    ) -> dict[str, Any]:
        """Quick check if a property identifier is authorized for an agent."""
        resp = await self._request_ok(
            "GET",
            "/api/registry/validate/property-authorization",
            params={
                "agent_url": agent_url,
                "identifier_type": identifier_type,
                "identifier_value": identifier_value,
            },
            operation="Property authorization",
        )

        return cast(dict[str, Any], resp.json())

    # ========================================================================
    # Validation Tools
    # ========================================================================

    async def validate_adagents(self, domain: str) -> dict[str, Any]:
        """Validate a domain's adagents.json via the registry API."""
        resp = await self._request_ok(
            "POST",
            "/api/adagents/validate",
            json_body={"domain": domain},
            operation="Adagents validate",
        )

        return cast(dict[str, Any], resp.json())

    async def create_adagents(
        self,
        authorized_agents: list[dict[str, Any]],
        *,
        include_schema: bool = False,
        include_timestamp: bool = False,
        properties: list[Any] | None = None,
    ) -> CreateAdagentsResponse:
        """Generate a valid adagents.json from authorized agents."""
        body: dict[str, Any] = {"authorized_agents": authorized_agents}
        if include_schema:
            body["include_schema"] = True
        if include_timestamp:
            body["include_timestamp"] = True
        if properties is not None:
            body["properties"] = properties
        resp = await self._request_ok(
            "POST",
            "/api/adagents/create",
            json_body=body,
            operation="Adagents create",
        )

        return self._parse(CreateAdagentsResponse, resp.json(), "Adagents create")

    # ========================================================================
    # Community Mirror Lifecycle
    # ========================================================================

    async def publish_community_mirror_adagents(
        self,
        platform: str,
        config: dict[str, Any],
        *,
        auth_token: str,
    ) -> CommunityMirrorPublishResponse:
        """Publish or update a catalog-only community mirror adagents.json descriptor.

        Persists the mirror under ``PUT /api/registry/mirrors/{platform}``. Use
        ``create_adagents`` (the generator endpoint) when you only need to
        validate or preview the document without saving it. The publish body is
        catalog-only; the service forces ``authorized_agents: []``.

        Args:
            platform: Stable platform key. Trimmed/lowercased and validated
                against ``^[a-z0-9_-]{1,64}$``.
            config: Catalog config (see ``build_community_mirror_adagents``).
                Any ``properties[].platform`` values must match ``platform``.
            auth_token: Bearer token required for save operations.

        Returns:
            The publish response.

        Raises:
            RegistryError: On platform/catalog validation or HTTP errors.
        """
        normalized_platform = _normalize_community_mirror_platform(platform)
        catalog = build_community_mirror_adagents(config)
        self._assert_community_mirror_properties_match_platform(normalized_platform, catalog)
        resp = await self._request_ok(
            "PUT",
            f"/api/registry/mirrors/{url_quote(normalized_platform, safe='')}",
            json_body=catalog,
            auth_token=auth_token,
            operation="Community mirror publish",
        )
        return self._parse(CommunityMirrorPublishResponse, resp.json(), "Community mirror publish")

    async def get_community_mirror_adagents(
        self, platform: str
    ) -> CommunityMirrorGetResponse | None:
        """Retrieve a published catalog-only community mirror adagents.json descriptor.

        Fetches ``GET /api/registry/mirrors/{platform}``. The response carries the
        platform metadata (``platform``, ``catalog_etag``, ``superseded_by``,
        ``created_at``, ``updated_at``) plus the stored catalog-only
        ``adagents_json`` document. ``superseded_by`` is reported at both the
        wrapper level and on ``adagents_json`` by the service, so no hydration is
        needed. The catalog-only invariant (``authorized_agents`` empty) is
        enforced by the response model.

        Args:
            platform: Platform key. Trimmed/lowercased and validated.

        Returns:
            The mirror response, or ``None`` if no mirror exists (HTTP 404).

        Raises:
            RegistryError: If the registry returns a mismatched platform or an
                invalid (non-catalog) mirror body, or on other HTTP errors.
        """
        normalized_platform = _normalize_community_mirror_platform(platform)
        resp = await self._request(
            "GET",
            f"/api/registry/mirrors/{url_quote(normalized_platform, safe='')}",
            operation="Community mirror fetch",
            allow_404=True,
        )
        if resp is None:
            return None
        mirror = self._parse(CommunityMirrorGetResponse, resp.json(), "Community mirror fetch")

        if mirror.platform != normalized_platform:
            raise RegistryError("Registry returned mismatched community mirror platform")
        return mirror

    async def list_community_mirror_adagents(
        self,
        *,
        limit: int | None = None,
        offset: int | None = None,
    ) -> CommunityMirrorListResponse:
        """List published community mirror catalogs with their current etags.

        Fetches ``GET /api/registry/mirrors``. The list projection includes
        presence and freshness metadata but omits the full ``adagents_json``
        body; fetch a platform-specific mirror for the full document.

        Args:
            limit: Optional page size. The service defaults to 100 and clamps
                values to the 1-500 range.
            offset: Optional zero-based result offset. The service defaults to 0
                and clamps negative values to 0.

        Returns:
            The list response (``mirrors`` summaries plus ``total``).
        """
        params: dict[str, Any] = {}
        if limit is not None:
            params["limit"] = limit
        if offset is not None:
            params["offset"] = offset
        resp = await self._request_ok(
            "GET",
            "/api/registry/mirrors",
            params=params or None,
            operation="Community mirror list",
        )
        return self._parse(CommunityMirrorListResponse, resp.json(), "Community mirror list")

    async def upsert_community_mirror_adagents(
        self,
        config: dict[str, Any],
        *,
        platform: str | None = None,
        auth_token: str,
    ) -> CommunityMirrorPublishResponse:
        """Publish or update a community mirror, inferring the platform key.

        The platform key is resolved from the ``platform`` argument, then
        ``config["platform"]``, then a single consistent ``properties[].platform``
        value. Ambiguous property platforms raise an error.

        Args:
            config: Catalog config (see ``build_community_mirror_adagents``).
            platform: Explicit platform key. Takes precedence over inference.
            auth_token: Bearer token required for save operations.

        Returns:
            The publish response.

        Raises:
            RegistryError: If a platform key cannot be resolved, property
                platforms are ambiguous, or on validation/HTTP errors.
        """
        resolved_platform = (
            platform
            if platform is not None
            else self._community_mirror_platform_from_config(config)
        )
        return await self.publish_community_mirror_adagents(
            resolved_platform, config, auth_token=auth_token
        )

    async def delete_community_mirror_adagents(
        self,
        platform: str,
        *,
        force: bool = False,
        auth_token: str,
    ) -> CommunityMirrorDeleteResponse:
        """Delete a published community mirror and retire its derived rows.

        Issues ``DELETE /api/registry/mirrors/{platform}``. Without ``force``,
        the service refuses (HTTP 409) to delete a mirror that has not first
        published a ``superseded_by`` migration URL; set ``force=True`` to delete
        anyway.

        Args:
            platform: Platform key. Trimmed/lowercased and validated.
            force: Delete a mirror that has no ``superseded_by`` migration URL.
            auth_token: Bearer token required for delete operations.

        Returns:
            The delete response.

        Raises:
            RegistryError: If the mirror has not been superseded and ``force`` is
                not set (HTTP 409), or on other platform/HTTP errors.
        """
        normalized_platform = _normalize_community_mirror_platform(platform)
        params = {"force": "true"} if force else None
        resp = await self._request_ok(
            "DELETE",
            f"/api/registry/mirrors/{url_quote(normalized_platform, safe='')}",
            params=params,
            auth_token=auth_token,
            operation="Community mirror delete",
        )
        return self._parse(CommunityMirrorDeleteResponse, resp.json(), "Community mirror delete")

    def _community_mirror_platform_from_config(self, config: dict[str, Any]) -> str:
        """Infer the platform key from a community mirror config."""
        config_platform = config.get("platform")
        if isinstance(config_platform, str) and config_platform.strip():
            return config_platform

        properties = config.get("properties")
        if isinstance(properties, list):
            platforms: set[str] = set()
            for prop in properties:
                if not isinstance(prop, dict):
                    continue
                prop_platform = prop.get("platform")
                if isinstance(prop_platform, str) and prop_platform.strip():
                    platforms.add(_normalize_community_mirror_platform(prop_platform))
            if len(platforms) == 1:
                return next(iter(platforms))
            if len(platforms) > 1:
                raise RegistryError(
                    "platform is ambiguous; pass "
                    "upsert_community_mirror_adagents(config, platform=...)"
                )

        raise RegistryError("platform is required for community mirror publish")

    def _assert_community_mirror_properties_match_platform(
        self,
        normalized_platform: str,
        catalog: dict[str, Any],
    ) -> None:
        """Reject catalogs whose property platforms disagree with the key."""
        properties = catalog.get("properties")
        if not isinstance(properties, list):
            return
        for prop in properties:
            if not isinstance(prop, dict):
                continue
            prop_platform = prop.get("platform")
            if prop_platform is None:
                continue
            if (
                not isinstance(prop_platform, str)
                or _normalize_community_mirror_platform(prop_platform) != normalized_platform
            ):
                raise RegistryError(f"properties[].platform must match {normalized_platform}")

    # ========================================================================
    # Search
    # ========================================================================

    async def api_discovery(self) -> dict[str, Any]:
        """Get API discovery info (links to entry points and docs)."""
        resp = await self._request_ok(
            "GET",
            "/api",
            operation="API discovery",
        )

        return cast(dict[str, Any], resp.json())

    async def search(self, q: str) -> dict[str, Any]:
        """Search across brands, publishers, and properties."""
        resp = await self._request_ok(
            "GET",
            "/api/search",
            params={"q": q},
            operation="Search",
        )

        return cast(dict[str, Any], resp.json())

    async def lookup_manifest_ref(self, domain: str, *, type: str | None = None) -> dict[str, Any]:
        """Find the best manifest reference for a domain."""
        params: dict[str, Any] = {"domain": domain}
        if type is not None:
            params["type"] = type
        resp = await self._request_ok(
            "GET",
            "/api/manifest-refs/lookup",
            params=params,
            operation="Manifest ref lookup",
        )

        return cast(dict[str, Any], resp.json())

    # ========================================================================
    # Agent Probing
    # ========================================================================

    async def discover_agent(self, url: str) -> dict[str, Any]:
        """Probe an agent URL to discover its capabilities."""
        resp = await self._request_ok(
            "GET",
            "/api/public/discover-agent",
            params={"url": url},
            operation="Agent discovery",
        )

        return cast(dict[str, Any], resp.json())

    async def get_agent_formats(self, url: str) -> dict[str, Any]:
        """Fetch creative formats from an agent."""
        resp = await self._request_ok(
            "GET",
            "/api/public/agent-formats",
            params={"url": url},
            operation="Agent formats",
        )

        return cast(dict[str, Any], resp.json())

    async def get_agent_products(self, url: str) -> dict[str, Any]:
        """Fetch products from a sales agent."""
        resp = await self._request_ok(
            "GET",
            "/api/public/agent-products",
            params={"url": url},
            operation="Agent products",
        )

        return cast(dict[str, Any], resp.json())

    async def validate_publisher(self, domain: str) -> dict[str, Any]:
        """Validate a publisher domain's adagents.json and return stats."""
        resp = await self._request_ok(
            "GET",
            "/api/public/validate-publisher",
            params={"domain": domain},
            operation="Publisher validation",
        )

        return cast(dict[str, Any], resp.json())

    # ========================================================================
    # Compliance, Verification, and Member Management
    # ========================================================================

    @staticmethod
    def _encoded_agent_url(agent_url: str) -> str:
        return url_quote(agent_url, safe="")

    async def get_agent_compliance(self, agent_url: str) -> dict[str, Any]:
        """Fetch the latest compliance summary for an agent."""
        encoded = self._encoded_agent_url(agent_url)
        return await self._request_json(
            "GET",
            f"/api/registry/agents/{encoded}/compliance",
            operation="Agent compliance",
        )

    async def get_jwks(self) -> dict[str, Any]:
        """Fetch the registry JWKS document."""
        return await self._request_json(
            "GET",
            "/api/.well-known/jwks.json",
            operation="Registry JWKS",
        )

    async def get_agent_verification(self, agent_url: str) -> dict[str, Any]:
        """Fetch the active verification badges for an agent."""
        encoded = self._encoded_agent_url(agent_url)
        return await self._request_json(
            "GET",
            f"/api/registry/agents/{encoded}/verification",
            operation="Agent verification",
        )

    async def get_agent_badge_svg(self, agent_url: str, role: str) -> str:
        """Fetch an agent verification badge SVG."""
        encoded = self._encoded_agent_url(agent_url)
        return await self._request_text(
            "GET",
            f"/api/registry/agents/{encoded}/badge/{url_quote(role, safe='')}.svg",
            operation="Agent badge SVG",
        )

    async def get_agent_badge_embed(self, agent_url: str, role: str) -> dict[str, Any]:
        """Fetch embeddable badge HTML/Markdown for an agent."""
        encoded = self._encoded_agent_url(agent_url)
        return await self._request_json(
            "GET",
            f"/api/registry/agents/{encoded}/badge/{url_quote(role, safe='')}/embed",
            operation="Agent badge embed",
        )

    async def get_agent_badge_versioned_svg(
        self,
        agent_url: str,
        role: str,
        version: str,
    ) -> str:
        """Fetch a version-pinned agent verification badge SVG."""
        encoded = self._encoded_agent_url(agent_url)
        return await self._request_text(
            "GET",
            "/api/registry/agents/"
            f"{encoded}/badge/{url_quote(role, safe='')}/{url_quote(version, safe='')}.svg",
            operation="Versioned agent badge SVG",
        )

    async def get_agent_badge_versioned_embed(
        self,
        agent_url: str,
        role: str,
        version: str,
    ) -> dict[str, Any]:
        """Fetch version-pinned embeddable badge HTML/Markdown for an agent."""
        encoded = self._encoded_agent_url(agent_url)
        return await self._request_json(
            "GET",
            "/api/registry/agents/"
            f"{encoded}/badge/{url_quote(role, safe='')}/{url_quote(version, safe='')}/embed",
            operation="Versioned agent badge embed",
        )

    async def get_agent_storyboard_status(self, agent_url: str) -> dict[str, Any]:
        """Fetch latest storyboard status for one agent."""
        encoded = self._encoded_agent_url(agent_url)
        return await self._request_json(
            "GET",
            f"/api/registry/agents/{encoded}/storyboard-status",
            operation="Agent storyboard status",
        )

    async def bulk_agent_storyboard_status(
        self,
        *,
        auth_token: str | None = None,
        **body: Any,
    ) -> dict[str, Any]:
        """Fetch storyboard status for multiple agents."""
        return await self._request_json(
            "POST",
            "/api/registry/agents/storyboard-status",
            json_body=dict(body),
            auth_token=auth_token,
            operation="Bulk agent storyboard status",
        )

    async def get_agent_compliance_history(
        self,
        agent_url: str,
        *,
        limit: int | None = None,
    ) -> dict[str, Any]:
        """Fetch compliance run history for an agent."""
        encoded = self._encoded_agent_url(agent_url)
        params = {"limit": limit} if limit is not None else None
        return await self._request_json(
            "GET",
            f"/api/registry/agents/{encoded}/compliance/history",
            params=params,
            operation="Agent compliance history",
        )

    async def update_agent_lifecycle(
        self,
        agent_url: str,
        *,
        auth_token: str,
        **body: Any,
    ) -> dict[str, Any]:
        """Update an agent lifecycle stage."""
        encoded = self._encoded_agent_url(agent_url)
        return await self._request_json(
            "PUT",
            f"/api/registry/agents/{encoded}/lifecycle",
            json_body=dict(body),
            auth_token=auth_token,
            operation="Agent lifecycle update",
        )

    async def update_agent_compliance_opt_out(
        self,
        agent_url: str,
        *,
        auth_token: str,
        **body: Any,
    ) -> dict[str, Any]:
        """Update agent compliance opt-out state."""
        encoded = self._encoded_agent_url(agent_url)
        return await self._request_json(
            "PUT",
            f"/api/registry/agents/{encoded}/compliance/opt-out",
            json_body=dict(body),
            auth_token=auth_token,
            operation="Agent compliance opt-out update",
        )

    async def get_agent_monitoring_settings(self, agent_url: str) -> dict[str, Any]:
        """Fetch monitoring settings for an agent."""
        encoded = self._encoded_agent_url(agent_url)
        return await self._request_json(
            "GET",
            f"/api/registry/agents/{encoded}/monitoring/settings",
            operation="Agent monitoring settings",
        )

    async def update_agent_monitoring_pause(
        self,
        agent_url: str,
        *,
        auth_token: str,
        **body: Any,
    ) -> dict[str, Any]:
        """Pause or resume monitoring for an agent."""
        encoded = self._encoded_agent_url(agent_url)
        return await self._request_json(
            "PUT",
            f"/api/registry/agents/{encoded}/monitoring/pause",
            json_body=dict(body),
            auth_token=auth_token,
            operation="Agent monitoring pause update",
        )

    async def update_agent_monitoring_interval(
        self,
        agent_url: str,
        *,
        auth_token: str,
        **body: Any,
    ) -> dict[str, Any]:
        """Update monitoring interval for an agent."""
        encoded = self._encoded_agent_url(agent_url)
        return await self._request_json(
            "PUT",
            f"/api/registry/agents/{encoded}/monitoring/interval",
            json_body=dict(body),
            auth_token=auth_token,
            operation="Agent monitoring interval update",
        )

    async def requeue_agent_for_heartbeat(
        self,
        agent_url: str,
        *,
        auth_token: str,
    ) -> dict[str, Any]:
        """Requeue an agent for heartbeat monitoring."""
        encoded = self._encoded_agent_url(agent_url)
        return await self._request_json(
            "POST",
            f"/api/registry/agents/{encoded}/monitoring/requeue",
            auth_token=auth_token,
            operation="Agent heartbeat requeue",
        )

    async def get_agent_compliance_step_diagnostics(
        self,
        agent_url: str,
        *,
        run_id: str | None = None,
        limit: int | None = None,
    ) -> dict[str, Any]:
        """Fetch compliance step diagnostics for an agent."""
        encoded = self._encoded_agent_url(agent_url)
        params = {k: v for k, v in {"run_id": run_id, "limit": limit}.items() if v is not None}
        return await self._request_json(
            "GET",
            f"/api/registry/agents/{encoded}/compliance/diagnostics",
            params=params,
            operation="Agent compliance step diagnostics",
        )

    async def get_agent_monitoring_requests(
        self,
        agent_url: str,
        *,
        limit: int | None = None,
        since: str | None = None,
    ) -> dict[str, Any]:
        """Fetch monitoring requests for an agent."""
        encoded = self._encoded_agent_url(agent_url)
        params = {k: v for k, v in {"limit": limit, "since": since}.items() if v is not None}
        return await self._request_json(
            "GET",
            f"/api/registry/agents/{encoded}/monitoring/requests",
            params=params,
            operation="Agent monitoring requests",
        )

    async def refresh_agent(
        self,
        agent_url: str,
        *,
        auth_token: str,
    ) -> dict[str, Any]:
        """Refresh an agent's registry health/capability/compliance snapshot."""
        encoded = self._encoded_agent_url(agent_url)
        return await self._request_json(
            "POST",
            f"/api/registry/agents/{encoded}/refresh",
            auth_token=auth_token,
            operation="Agent refresh",
            expected_status={200, 202},
        )

    async def get_agent_auth_status(self, agent_url: str) -> dict[str, Any]:
        """Fetch saved authentication status for an agent."""
        encoded = self._encoded_agent_url(agent_url)
        return await self._request_json(
            "GET",
            f"/api/registry/agents/{encoded}/auth-status",
            operation="Agent auth status",
        )

    async def connect_agent(
        self,
        agent_url: str,
        *,
        auth_token: str,
        **body: Any,
    ) -> dict[str, Any]:
        """Connect registry-managed credentials for an agent."""
        encoded = self._encoded_agent_url(agent_url)
        return await self._request_json(
            "PUT",
            f"/api/registry/agents/{encoded}/connect",
            json_body=dict(body),
            auth_token=auth_token,
            operation="Agent connect",
        )

    async def save_agent_oauth_client_credentials(
        self,
        agent_url: str,
        *,
        auth_token: str,
        **body: Any,
    ) -> dict[str, Any]:
        """Save OAuth client credentials for an agent."""
        encoded = self._encoded_agent_url(agent_url)
        return await self._request_json(
            "PUT",
            f"/api/registry/agents/{encoded}/oauth-client-credentials",
            json_body=dict(body),
            auth_token=auth_token,
            operation="Agent OAuth client credentials save",
        )

    async def test_agent_oauth_client_credentials(
        self,
        agent_url: str,
        *,
        auth_token: str,
    ) -> dict[str, Any]:
        """Test saved OAuth client credentials for an agent."""
        encoded = self._encoded_agent_url(agent_url)
        return await self._request_json(
            "POST",
            f"/api/registry/agents/{encoded}/oauth-client-credentials/test",
            auth_token=auth_token,
            operation="Agent OAuth client credentials test",
        )

    async def get_applicable_storyboards(self, agent_url: str) -> dict[str, Any]:
        """Resolve compliance storyboards applicable to an agent."""
        encoded = self._encoded_agent_url(agent_url)
        return await self._request_json(
            "GET",
            f"/api/registry/agents/{encoded}/applicable-storyboards",
            operation="Applicable storyboards",
        )

    async def list_storyboards(
        self,
        *,
        category: str | None = None,
        compliance_target: str | None = None,
    ) -> dict[str, Any]:
        """List available registry compliance storyboards."""
        params = {
            k: v
            for k, v in {"category": category, "compliance_target": compliance_target}.items()
            if v is not None
        }
        return await self._request_json(
            "GET",
            "/api/storyboards",
            params=params,
            operation="Storyboard list",
        )

    async def get_storyboard(
        self,
        storyboard_id: str,
        *,
        compliance_target: str | None = None,
    ) -> dict[str, Any]:
        """Fetch one registry compliance storyboard."""
        params = {"compliance_target": compliance_target} if compliance_target is not None else None
        return await self._request_json(
            "GET",
            f"/api/storyboards/{url_quote(storyboard_id, safe='')}",
            params=params,
            operation="Storyboard get",
        )

    async def find_brand(self, q: str, *, limit: int | None = None) -> dict[str, Any]:
        """Find brands by name or domain."""
        params: dict[str, Any] = {"q": q}
        if limit is not None:
            params["limit"] = limit
        return await self._request_json(
            "GET",
            "/api/brands/find",
            params=params,
            operation="Brand find",
        )

    async def setup_my_brand(self, *, auth_token: str, **body: Any) -> dict[str, Any]:
        """Set up a brand record for the authenticated member."""
        return await self._request_json(
            "POST",
            "/api/brands/setup-my-brand",
            json_body=dict(body),
            auth_token=auth_token,
            operation="Brand setup",
        )

    async def bulk_property_check(
        self,
        *,
        auth_token: str | None = None,
        **body: Any,
    ) -> dict[str, Any]:
        """Start a bulk property check."""
        return await self._request_json(
            "POST",
            "/api/properties/check/bulk",
            json_body=dict(body),
            auth_token=auth_token,
            operation="Bulk property check",
            expected_status={200, 202},
        )

    async def get_bulk_property_check_report(self, report_id: str) -> dict[str, Any]:
        """Retrieve a bulk property check report by ID."""
        return await self._request_json(
            "GET",
            f"/api/properties/check/bulk/{url_quote(report_id, safe='')}",
            operation="Bulk property check report",
        )

    async def run_storyboard_step(
        self,
        agent_url: str,
        storyboard_id: str,
        step_id: str,
        *,
        auth_token: str,
        **body: Any,
    ) -> dict[str, Any]:
        """Run one storyboard step against an agent."""
        encoded = self._encoded_agent_url(agent_url)
        return await self._request_json(
            "POST",
            "/api/registry/agents/"
            f"{encoded}/storyboard/{url_quote(storyboard_id, safe='')}/step/"
            f"{url_quote(step_id, safe='')}",
            json_body=dict(body),
            auth_token=auth_token,
            operation="Storyboard step run",
        )

    async def get_storyboard_first_step(
        self,
        storyboard_id: str,
        *,
        compliance_target: str | None = None,
    ) -> dict[str, Any]:
        """Fetch the first runnable step for a storyboard."""
        params = {"compliance_target": compliance_target} if compliance_target is not None else None
        return await self._request_json(
            "GET",
            f"/api/storyboards/{url_quote(storyboard_id, safe='')}/first-step",
            params=params,
            operation="Storyboard first step",
        )

    async def run_storyboard(
        self,
        agent_url: str,
        storyboard_id: str,
        *,
        auth_token: str,
    ) -> dict[str, Any]:
        """Run a storyboard against an agent."""
        encoded = self._encoded_agent_url(agent_url)
        return await self._request_json(
            "POST",
            f"/api/registry/agents/{encoded}/storyboard/{url_quote(storyboard_id, safe='')}/run",
            auth_token=auth_token,
            operation="Storyboard run",
            expected_status={200, 202},
        )

    async def compare_storyboard(
        self,
        agent_url: str,
        storyboard_id: str,
        *,
        auth_token: str,
    ) -> dict[str, Any]:
        """Compare storyboard runs for an agent."""
        encoded = self._encoded_agent_url(agent_url)
        return await self._request_json(
            "POST",
            "/api/registry/agents/"
            f"{encoded}/storyboard/{url_quote(storyboard_id, safe='')}/compare",
            auth_token=auth_token,
            operation="Storyboard compare",
        )

    async def list_member_agents(
        self,
        *,
        auth_token: str,
        org: str | None = None,
    ) -> dict[str, Any]:
        """List the authenticated member's registered agents."""
        params = {"org": org} if org is not None else None
        return await self._request_json(
            "GET",
            "/api/me/agents",
            params=params,
            auth_token=auth_token,
            operation="Member agent list",
        )

    async def register_member_agent(
        self,
        *,
        auth_token: str,
        org: str | None = None,
        **body: Any,
    ) -> dict[str, Any]:
        """Register an agent for the authenticated member."""
        params = {"org": org} if org is not None else None
        return await self._request_json(
            "POST",
            "/api/me/agents",
            params=params,
            json_body=dict(body),
            auth_token=auth_token,
            operation="Member agent register",
            expected_status={200, 201},
        )

    async def update_member_agent(
        self,
        url: str,
        *,
        auth_token: str,
        org: str | None = None,
        **body: Any,
    ) -> dict[str, Any]:
        """Update one member-owned agent."""
        params = {"org": org} if org is not None else None
        return await self._request_json(
            "PATCH",
            f"/api/me/agents/{url_quote(url, safe='')}",
            params=params,
            json_body=dict(body),
            auth_token=auth_token,
            operation="Member agent update",
        )

    async def remove_member_agent(
        self,
        url: str,
        *,
        auth_token: str,
        org: str | None = None,
    ) -> dict[str, Any]:
        """Remove one member-owned agent."""
        params = {"org": org} if org is not None else None
        return await self._request_json(
            "DELETE",
            f"/api/me/agents/{url_quote(url, safe='')}",
            params=params,
            auth_token=auth_token,
            operation="Member agent remove",
        )

    async def create_organization(self, *, auth_token: str, **body: Any) -> dict[str, Any]:
        """Create an organization for the authenticated member."""
        return await self._request_json(
            "POST",
            "/api/organizations",
            json_body=dict(body),
            auth_token=auth_token,
            operation="Organization create",
            expected_status={200, 201},
        )

    # ========================================================================
    # Change Feed
    # ========================================================================

    async def get_feed(
        self,
        *,
        auth_token: str,
        cursor: str | None = None,
        types: str | None = None,
        limit: int = 100,
    ) -> FeedPage:
        """Poll the registry change feed (auth required).

        Returns a FeedPage with events, cursor, and has_more.
        Pass cursor from previous response to resume.
        """
        params: dict[str, Any] = {"limit": limit}
        if cursor is not None:
            params["cursor"] = cursor
        if types is not None:
            params["types"] = types
        resp = await self._request_ok(
            "GET",
            "/api/registry/feed",
            params=params,
            auth_token=auth_token,
            operation="Feed poll",
        )

        return self._parse(FeedPage, resp.json(), "Feed poll")

Client for the AdCP registry API.

Provides brand, property, and member lookups against the central AdCP registry.

Args
-----=
base_url
Registry API base URL.
timeout
Request timeout in seconds.
client
Optional httpx.AsyncClient for connection pooling. If provided, caller is responsible for client lifecycle.
user_agent
User-Agent header for requests.

Methods

async def api_discovery(self) ‑> dict[str, typing.Any]
Expand source code
async def api_discovery(self) -> dict[str, Any]:
    """Get API discovery info (links to entry points and docs)."""
    resp = await self._request_ok(
        "GET",
        "/api",
        operation="API discovery",
    )

    return cast(dict[str, Any], resp.json())

Get API discovery info (links to entry points and docs).

async def brand_history(self, domain: str, limit: int = 20, offset: int = 0) ‑> BrandActivity | None
Expand source code
async def brand_history(
    self,
    domain: str,
    limit: int = 20,
    offset: int = 0,
) -> BrandActivity | None:
    """Get edit history for a brand."""
    resp = await self._request(
        "GET",
        "/api/brands/history",
        params={"domain": domain, "limit": limit, "offset": offset},
        allow_404=True,
        operation="Brand history",
    )
    if resp is None:
        return None
    return self._parse(BrandActivity, resp.json(), "Brand history")

Get edit history for a brand.

async def bulk_agent_storyboard_status(self, *, auth_token: str | None = None, **body: Any) ‑> dict[str, typing.Any]
Expand source code
async def bulk_agent_storyboard_status(
    self,
    *,
    auth_token: str | None = None,
    **body: Any,
) -> dict[str, Any]:
    """Fetch storyboard status for multiple agents."""
    return await self._request_json(
        "POST",
        "/api/registry/agents/storyboard-status",
        json_body=dict(body),
        auth_token=auth_token,
        operation="Bulk agent storyboard status",
    )

Fetch storyboard status for multiple agents.

async def bulk_property_check(self, *, auth_token: str | None = None, **body: Any) ‑> dict[str, typing.Any]
Expand source code
async def bulk_property_check(
    self,
    *,
    auth_token: str | None = None,
    **body: Any,
) -> dict[str, Any]:
    """Start a bulk property check."""
    return await self._request_json(
        "POST",
        "/api/properties/check/bulk",
        json_body=dict(body),
        auth_token=auth_token,
        operation="Bulk property check",
        expected_status={200, 202},
    )

Start a bulk property check.

async def check_property_list(self, domains: list[str]) ‑> dict[str, typing.Any]
Expand source code
async def check_property_list(self, domains: list[str]) -> dict[str, Any]:
    """Check publisher domains against the registry."""
    resp = await self._request_ok(
        "POST",
        "/api/properties/check",
        json_body={"domains": domains},
        operation="Property check",
    )

    return cast(dict[str, Any], resp.json())

Check publisher domains against the registry.

async def close(self) ‑> None
Expand source code
async def close(self) -> None:
    """Close owned HTTP client. No-op if using external client."""
    if self._owned_client is not None:
        await self._owned_client.aclose()
        self._owned_client = None

Close owned HTTP client. No-op if using external client.

async def compare_storyboard(self, agent_url: str, storyboard_id: str, *, auth_token: str) ‑> dict[str, typing.Any]
Expand source code
async def compare_storyboard(
    self,
    agent_url: str,
    storyboard_id: str,
    *,
    auth_token: str,
) -> dict[str, Any]:
    """Compare storyboard runs for an agent."""
    encoded = self._encoded_agent_url(agent_url)
    return await self._request_json(
        "POST",
        "/api/registry/agents/"
        f"{encoded}/storyboard/{url_quote(storyboard_id, safe='')}/compare",
        auth_token=auth_token,
        operation="Storyboard compare",
    )

Compare storyboard runs for an agent.

async def connect_agent(self, agent_url: str, *, auth_token: str, **body: Any) ‑> dict[str, typing.Any]
Expand source code
async def connect_agent(
    self,
    agent_url: str,
    *,
    auth_token: str,
    **body: Any,
) -> dict[str, Any]:
    """Connect registry-managed credentials for an agent."""
    encoded = self._encoded_agent_url(agent_url)
    return await self._request_json(
        "PUT",
        f"/api/registry/agents/{encoded}/connect",
        json_body=dict(body),
        auth_token=auth_token,
        operation="Agent connect",
    )

Connect registry-managed credentials for an agent.

async def create_adagents(self,
authorized_agents: list[dict[str, Any]],
*,
include_schema: bool = False,
include_timestamp: bool = False,
properties: list[Any] | None = None) ‑> CreateAdagentsResponse
Expand source code
async def create_adagents(
    self,
    authorized_agents: list[dict[str, Any]],
    *,
    include_schema: bool = False,
    include_timestamp: bool = False,
    properties: list[Any] | None = None,
) -> CreateAdagentsResponse:
    """Generate a valid adagents.json from authorized agents."""
    body: dict[str, Any] = {"authorized_agents": authorized_agents}
    if include_schema:
        body["include_schema"] = True
    if include_timestamp:
        body["include_timestamp"] = True
    if properties is not None:
        body["properties"] = properties
    resp = await self._request_ok(
        "POST",
        "/api/adagents/create",
        json_body=body,
        operation="Adagents create",
    )

    return self._parse(CreateAdagentsResponse, resp.json(), "Adagents create")

Generate a valid adagents.json from authorized agents.

async def create_organization(self, *, auth_token: str, **body: Any) ‑> dict[str, typing.Any]
Expand source code
async def create_organization(self, *, auth_token: str, **body: Any) -> dict[str, Any]:
    """Create an organization for the authenticated member."""
    return await self._request_json(
        "POST",
        "/api/organizations",
        json_body=dict(body),
        auth_token=auth_token,
        operation="Organization create",
        expected_status={200, 201},
    )

Create an organization for the authenticated member.

async def delete_community_mirror_adagents(self, platform: str, *, force: bool = False, auth_token: str) ‑> CommunityMirrorDeleteResponse
Expand source code
async def delete_community_mirror_adagents(
    self,
    platform: str,
    *,
    force: bool = False,
    auth_token: str,
) -> CommunityMirrorDeleteResponse:
    """Delete a published community mirror and retire its derived rows.

    Issues ``DELETE /api/registry/mirrors/{platform}``. Without ``force``,
    the service refuses (HTTP 409) to delete a mirror that has not first
    published a ``superseded_by`` migration URL; set ``force=True`` to delete
    anyway.

    Args:
        platform: Platform key. Trimmed/lowercased and validated.
        force: Delete a mirror that has no ``superseded_by`` migration URL.
        auth_token: Bearer token required for delete operations.

    Returns:
        The delete response.

    Raises:
        RegistryError: If the mirror has not been superseded and ``force`` is
            not set (HTTP 409), or on other platform/HTTP errors.
    """
    normalized_platform = _normalize_community_mirror_platform(platform)
    params = {"force": "true"} if force else None
    resp = await self._request_ok(
        "DELETE",
        f"/api/registry/mirrors/{url_quote(normalized_platform, safe='')}",
        params=params,
        auth_token=auth_token,
        operation="Community mirror delete",
    )
    return self._parse(CommunityMirrorDeleteResponse, resp.json(), "Community mirror delete")

Delete a published community mirror and retire its derived rows.

Issues DELETE /api/registry/mirrors/{platform}. Without force, the service refuses (HTTP 409) to delete a mirror that has not first published a superseded_by migration URL; set force=True to delete anyway.

Args
-----=
platform
Platform key. Trimmed/lowercased and validated.
force
Delete a mirror that has no superseded_by migration URL.
auth_token
Bearer token required for delete operations.

Returns -----= The delete response.

Raises
-----=
RegistryError
If the mirror has not been superseded and force is not set (HTTP 409), or on other platform/HTTP errors.
async def discover_agent(self, url: str) ‑> dict[str, typing.Any]
Expand source code
async def discover_agent(self, url: str) -> dict[str, Any]:
    """Probe an agent URL to discover its capabilities."""
    resp = await self._request_ok(
        "GET",
        "/api/public/discover-agent",
        params={"url": url},
        operation="Agent discovery",
    )

    return cast(dict[str, Any], resp.json())

Probe an agent URL to discover its capabilities.

async def enrich_brand(self, domain: str) ‑> dict[str, typing.Any]
Expand source code
async def enrich_brand(self, domain: str) -> dict[str, Any]:
    """Enrich brand data using Brandfetch."""
    resp = await self._request_ok(
        "GET",
        "/api/brands/enrich",
        params={"domain": domain},
        operation="Brand enrich",
    )

    return cast(dict[str, Any], resp.json())

Enrich brand data using Brandfetch.

async def expand_product_identifiers(self, agent_url: str, publisher_properties: list[dict[str, Any]]) ‑> dict[str, typing.Any]
Expand source code
async def expand_product_identifiers(
    self,
    agent_url: str,
    publisher_properties: list[dict[str, Any]],
) -> dict[str, Any]:
    """Expand publisher_properties selectors into concrete identifiers."""
    resp = await self._request_ok(
        "POST",
        "/api/registry/expand/product-identifiers",
        json_body={
            "agent_url": agent_url,
            "publisher_properties": publisher_properties,
        },
        operation="Expand product identifiers",
    )

    return cast(dict[str, Any], resp.json())

Expand publisher_properties selectors into concrete identifiers.

async def find_brand(self, q: str, *, limit: int | None = None) ‑> dict[str, typing.Any]
Expand source code
async def find_brand(self, q: str, *, limit: int | None = None) -> dict[str, Any]:
    """Find brands by name or domain."""
    params: dict[str, Any] = {"q": q}
    if limit is not None:
        params["limit"] = limit
    return await self._request_json(
        "GET",
        "/api/brands/find",
        params=params,
        operation="Brand find",
    )

Find brands by name or domain.

async def get_agent_auth_status(self, agent_url: str) ‑> dict[str, typing.Any]
Expand source code
async def get_agent_auth_status(self, agent_url: str) -> dict[str, Any]:
    """Fetch saved authentication status for an agent."""
    encoded = self._encoded_agent_url(agent_url)
    return await self._request_json(
        "GET",
        f"/api/registry/agents/{encoded}/auth-status",
        operation="Agent auth status",
    )

Fetch saved authentication status for an agent.

async def get_agent_authorizations(self,
agent_url: str,
*,
auth_token: str | None = None,
include: str | None = None,
evidence: str | None = None) ‑> dict[str, typing.Any]
Expand source code
async def get_agent_authorizations(
    self,
    agent_url: str,
    *,
    auth_token: str | None = None,
    include: str | None = None,
    evidence: str | None = None,
) -> dict[str, Any]:
    """Fetch authorization rows for one agent."""
    params = {"agent_url": agent_url}
    if include is not None:
        params["include"] = include
    if evidence is not None:
        params["evidence"] = evidence
    return await self._request_json(
        "GET",
        "/api/registry/authorizations",
        params=params,
        auth_token=auth_token,
        operation="Agent authorizations",
    )

Fetch authorization rows for one agent.

async def get_agent_authorizations_snapshot(self,
*,
auth_token: str | None = None,
include: str | None = None,
evidence: str | None = None) ‑> dict[str, typing.Any]
Expand source code
async def get_agent_authorizations_snapshot(
    self,
    *,
    auth_token: str | None = None,
    include: str | None = None,
    evidence: str | None = None,
) -> dict[str, Any]:
    """Fetch the full authorization snapshot metadata."""
    params = {k: v for k, v in {"include": include, "evidence": evidence}.items() if v}
    return await self._request_json(
        "GET",
        "/api/registry/authorizations/snapshot",
        params=params,
        auth_token=auth_token,
        operation="Agent authorizations snapshot",
    )

Fetch the full authorization snapshot metadata.

async def get_agent_badge_embed(self, agent_url: str, role: str) ‑> dict[str, typing.Any]
Expand source code
async def get_agent_badge_embed(self, agent_url: str, role: str) -> dict[str, Any]:
    """Fetch embeddable badge HTML/Markdown for an agent."""
    encoded = self._encoded_agent_url(agent_url)
    return await self._request_json(
        "GET",
        f"/api/registry/agents/{encoded}/badge/{url_quote(role, safe='')}/embed",
        operation="Agent badge embed",
    )

Fetch embeddable badge HTML/Markdown for an agent.

async def get_agent_badge_svg(self, agent_url: str, role: str) ‑> str
Expand source code
async def get_agent_badge_svg(self, agent_url: str, role: str) -> str:
    """Fetch an agent verification badge SVG."""
    encoded = self._encoded_agent_url(agent_url)
    return await self._request_text(
        "GET",
        f"/api/registry/agents/{encoded}/badge/{url_quote(role, safe='')}.svg",
        operation="Agent badge SVG",
    )

Fetch an agent verification badge SVG.

async def get_agent_badge_versioned_embed(self, agent_url: str, role: str, version: str) ‑> dict[str, typing.Any]
Expand source code
async def get_agent_badge_versioned_embed(
    self,
    agent_url: str,
    role: str,
    version: str,
) -> dict[str, Any]:
    """Fetch version-pinned embeddable badge HTML/Markdown for an agent."""
    encoded = self._encoded_agent_url(agent_url)
    return await self._request_json(
        "GET",
        "/api/registry/agents/"
        f"{encoded}/badge/{url_quote(role, safe='')}/{url_quote(version, safe='')}/embed",
        operation="Versioned agent badge embed",
    )

Fetch version-pinned embeddable badge HTML/Markdown for an agent.

async def get_agent_badge_versioned_svg(self, agent_url: str, role: str, version: str) ‑> str
Expand source code
async def get_agent_badge_versioned_svg(
    self,
    agent_url: str,
    role: str,
    version: str,
) -> str:
    """Fetch a version-pinned agent verification badge SVG."""
    encoded = self._encoded_agent_url(agent_url)
    return await self._request_text(
        "GET",
        "/api/registry/agents/"
        f"{encoded}/badge/{url_quote(role, safe='')}/{url_quote(version, safe='')}.svg",
        operation="Versioned agent badge SVG",
    )

Fetch a version-pinned agent verification badge SVG.

async def get_agent_compliance(self, agent_url: str) ‑> dict[str, typing.Any]
Expand source code
async def get_agent_compliance(self, agent_url: str) -> dict[str, Any]:
    """Fetch the latest compliance summary for an agent."""
    encoded = self._encoded_agent_url(agent_url)
    return await self._request_json(
        "GET",
        f"/api/registry/agents/{encoded}/compliance",
        operation="Agent compliance",
    )

Fetch the latest compliance summary for an agent.

async def get_agent_compliance_history(self, agent_url: str, *, limit: int | None = None) ‑> dict[str, typing.Any]
Expand source code
async def get_agent_compliance_history(
    self,
    agent_url: str,
    *,
    limit: int | None = None,
) -> dict[str, Any]:
    """Fetch compliance run history for an agent."""
    encoded = self._encoded_agent_url(agent_url)
    params = {"limit": limit} if limit is not None else None
    return await self._request_json(
        "GET",
        f"/api/registry/agents/{encoded}/compliance/history",
        params=params,
        operation="Agent compliance history",
    )

Fetch compliance run history for an agent.

async def get_agent_compliance_step_diagnostics(self, agent_url: str, *, run_id: str | None = None, limit: int | None = None) ‑> dict[str, typing.Any]
Expand source code
async def get_agent_compliance_step_diagnostics(
    self,
    agent_url: str,
    *,
    run_id: str | None = None,
    limit: int | None = None,
) -> dict[str, Any]:
    """Fetch compliance step diagnostics for an agent."""
    encoded = self._encoded_agent_url(agent_url)
    params = {k: v for k, v in {"run_id": run_id, "limit": limit}.items() if v is not None}
    return await self._request_json(
        "GET",
        f"/api/registry/agents/{encoded}/compliance/diagnostics",
        params=params,
        operation="Agent compliance step diagnostics",
    )

Fetch compliance step diagnostics for an agent.

async def get_agent_domains(self, agent_url: str) ‑> dict[str, typing.Any]
Expand source code
async def get_agent_domains(self, agent_url: str) -> dict[str, Any]:
    """Get all publisher domains and identifiers for an agent."""
    encoded = url_quote(agent_url, safe="")
    resp = await self._request_ok(
        "GET",
        f"/api/registry/lookup/agent/{encoded}/domains",
        operation="Agent domains lookup",
    )

    return cast(dict[str, Any], resp.json())

Get all publisher domains and identifiers for an agent.

async def get_agent_formats(self, url: str) ‑> dict[str, typing.Any]
Expand source code
async def get_agent_formats(self, url: str) -> dict[str, Any]:
    """Fetch creative formats from an agent."""
    resp = await self._request_ok(
        "GET",
        "/api/public/agent-formats",
        params={"url": url},
        operation="Agent formats",
    )

    return cast(dict[str, Any], resp.json())

Fetch creative formats from an agent.

async def get_agent_monitoring_requests(self, agent_url: str, *, limit: int | None = None, since: str | None = None) ‑> dict[str, typing.Any]
Expand source code
async def get_agent_monitoring_requests(
    self,
    agent_url: str,
    *,
    limit: int | None = None,
    since: str | None = None,
) -> dict[str, Any]:
    """Fetch monitoring requests for an agent."""
    encoded = self._encoded_agent_url(agent_url)
    params = {k: v for k, v in {"limit": limit, "since": since}.items() if v is not None}
    return await self._request_json(
        "GET",
        f"/api/registry/agents/{encoded}/monitoring/requests",
        params=params,
        operation="Agent monitoring requests",
    )

Fetch monitoring requests for an agent.

async def get_agent_monitoring_settings(self, agent_url: str) ‑> dict[str, typing.Any]
Expand source code
async def get_agent_monitoring_settings(self, agent_url: str) -> dict[str, Any]:
    """Fetch monitoring settings for an agent."""
    encoded = self._encoded_agent_url(agent_url)
    return await self._request_json(
        "GET",
        f"/api/registry/agents/{encoded}/monitoring/settings",
        operation="Agent monitoring settings",
    )

Fetch monitoring settings for an agent.

async def get_agent_products(self, url: str) ‑> dict[str, typing.Any]
Expand source code
async def get_agent_products(self, url: str) -> dict[str, Any]:
    """Fetch products from a sales agent."""
    resp = await self._request_ok(
        "GET",
        "/api/public/agent-products",
        params={"url": url},
        operation="Agent products",
    )

    return cast(dict[str, Any], resp.json())

Fetch products from a sales agent.

async def get_agent_storyboard_status(self, agent_url: str) ‑> dict[str, typing.Any]
Expand source code
async def get_agent_storyboard_status(self, agent_url: str) -> dict[str, Any]:
    """Fetch latest storyboard status for one agent."""
    encoded = self._encoded_agent_url(agent_url)
    return await self._request_json(
        "GET",
        f"/api/registry/agents/{encoded}/storyboard-status",
        operation="Agent storyboard status",
    )

Fetch latest storyboard status for one agent.

async def get_agent_verification(self, agent_url: str) ‑> dict[str, typing.Any]
Expand source code
async def get_agent_verification(self, agent_url: str) -> dict[str, Any]:
    """Fetch the active verification badges for an agent."""
    encoded = self._encoded_agent_url(agent_url)
    return await self._request_json(
        "GET",
        f"/api/registry/agents/{encoded}/verification",
        operation="Agent verification",
    )

Fetch the active verification badges for an agent.

async def get_applicable_storyboards(self, agent_url: str) ‑> dict[str, typing.Any]
Expand source code
async def get_applicable_storyboards(self, agent_url: str) -> dict[str, Any]:
    """Resolve compliance storyboards applicable to an agent."""
    encoded = self._encoded_agent_url(agent_url)
    return await self._request_json(
        "GET",
        f"/api/registry/agents/{encoded}/applicable-storyboards",
        operation="Applicable storyboards",
    )

Resolve compliance storyboards applicable to an agent.

async def get_brand_json(self, domain: str, *, fresh: bool = False) ‑> dict[str, typing.Any] | None
Expand source code
async def get_brand_json(self, domain: str, *, fresh: bool = False) -> dict[str, Any] | None:
    """Fetch raw brand.json for a domain."""
    params: dict[str, Any] = {"domain": domain}
    if fresh:
        params["fresh"] = "true"
    resp = await self._request(
        "GET",
        "/api/brands/brand-json",
        params=params,
        allow_404=True,
        operation="Brand JSON fetch",
    )
    if resp is None:
        return None
    return cast(dict[str, Any], resp.json())

Fetch raw brand.json for a domain.

async def get_bulk_property_check_report(self, report_id: str) ‑> dict[str, typing.Any]
Expand source code
async def get_bulk_property_check_report(self, report_id: str) -> dict[str, Any]:
    """Retrieve a bulk property check report by ID."""
    return await self._request_json(
        "GET",
        f"/api/properties/check/bulk/{url_quote(report_id, safe='')}",
        operation="Bulk property check report",
    )

Retrieve a bulk property check report by ID.

async def get_community_mirror_adagents(self, platform: str) ‑> CommunityMirrorGetResponse | None
Expand source code
async def get_community_mirror_adagents(
    self, platform: str
) -> CommunityMirrorGetResponse | None:
    """Retrieve a published catalog-only community mirror adagents.json descriptor.

    Fetches ``GET /api/registry/mirrors/{platform}``. The response carries the
    platform metadata (``platform``, ``catalog_etag``, ``superseded_by``,
    ``created_at``, ``updated_at``) plus the stored catalog-only
    ``adagents_json`` document. ``superseded_by`` is reported at both the
    wrapper level and on ``adagents_json`` by the service, so no hydration is
    needed. The catalog-only invariant (``authorized_agents`` empty) is
    enforced by the response model.

    Args:
        platform: Platform key. Trimmed/lowercased and validated.

    Returns:
        The mirror response, or ``None`` if no mirror exists (HTTP 404).

    Raises:
        RegistryError: If the registry returns a mismatched platform or an
            invalid (non-catalog) mirror body, or on other HTTP errors.
    """
    normalized_platform = _normalize_community_mirror_platform(platform)
    resp = await self._request(
        "GET",
        f"/api/registry/mirrors/{url_quote(normalized_platform, safe='')}",
        operation="Community mirror fetch",
        allow_404=True,
    )
    if resp is None:
        return None
    mirror = self._parse(CommunityMirrorGetResponse, resp.json(), "Community mirror fetch")

    if mirror.platform != normalized_platform:
        raise RegistryError("Registry returned mismatched community mirror platform")
    return mirror

Retrieve a published catalog-only community mirror adagents.json descriptor.

Fetches GET /api/registry/mirrors/{platform}. The response carries the platform metadata (platform, catalog_etag, superseded_by, created_at, updated_at) plus the stored catalog-only adagents_json document. superseded_by is reported at both the wrapper level and on adagents_json by the service, so no hydration is needed. The catalog-only invariant (authorized_agents empty) is enforced by the response model.

Args
-----=
platform
Platform key. Trimmed/lowercased and validated.

Returns -----= The mirror response, or None if no mirror exists (HTTP 404).

Raises
-----=
RegistryError
If the registry returns a mismatched platform or an invalid (non-catalog) mirror body, or on other HTTP errors.
async def get_feed(self,
*,
auth_token: str,
cursor: str | None = None,
types: str | None = None,
limit: int = 100) ‑> FeedPage
Expand source code
async def get_feed(
    self,
    *,
    auth_token: str,
    cursor: str | None = None,
    types: str | None = None,
    limit: int = 100,
) -> FeedPage:
    """Poll the registry change feed (auth required).

    Returns a FeedPage with events, cursor, and has_more.
    Pass cursor from previous response to resume.
    """
    params: dict[str, Any] = {"limit": limit}
    if cursor is not None:
        params["cursor"] = cursor
    if types is not None:
        params["types"] = types
    resp = await self._request_ok(
        "GET",
        "/api/registry/feed",
        params=params,
        auth_token=auth_token,
        operation="Feed poll",
    )

    return self._parse(FeedPage, resp.json(), "Feed poll")

Poll the registry change feed (auth required).

Returns a FeedPage with events, cursor, and has_more. Pass cursor from previous response to resume.

async def get_jwks(self) ‑> dict[str, typing.Any]
Expand source code
async def get_jwks(self) -> dict[str, Any]:
    """Fetch the registry JWKS document."""
    return await self._request_json(
        "GET",
        "/api/.well-known/jwks.json",
        operation="Registry JWKS",
    )

Fetch the registry JWKS document.

async def get_member(self, slug: str) ‑> Member | None
Expand source code
async def get_member(self, slug: str) -> Member | None:
    """Get a single AAO member by their slug.

    Args:
        slug: Member slug (e.g., "adgentek").

    Returns:
        Member if found, None if not in the registry.

    Raises:
        RegistryError: On HTTP or parsing errors.
        ValueError: If slug contains path-traversal characters.
    """
    if not slug or not re.fullmatch(r"[a-zA-Z0-9_-]+", slug):
        raise ValueError(f"Invalid member slug: {slug!r}")
    client = await self._get_client()
    try:
        response = await client.get(
            f"{self._base_url}/api/members/{slug}",
            headers={"User-Agent": self._user_agent},
            timeout=self._timeout,
        )
        if response.status_code == 404:
            return None
        if response.status_code != 200:
            raise _registry_http_error(
                response,
                method="GET",
                operation="Member lookup",
            )
        data = response.json()
        if data is None:
            return None
        return Member.model_validate(data)
    except RegistryError:
        raise
    except httpx.TimeoutException as e:
        raise RegistryError(f"Member lookup timed out after {self._timeout}s") from e
    except httpx.HTTPError as e:
        raise RegistryError(f"Member lookup failed: {e}") from e
    except (ValidationError, ValueError) as e:
        raise RegistryError(f"Member lookup failed: invalid response: {e}") from e

Get a single AAO member by their slug.

Args
-----=
slug
Member slug (e.g., "adgentek").

Returns -----= Member if found, None if not in the registry.

Raises
-----=
RegistryError
On HTTP or parsing errors.
ValueError
If slug contains path-traversal characters.
async def get_property_check_report(self, report_id: str) ‑> dict[str, typing.Any] | None
Expand source code
async def get_property_check_report(self, report_id: str) -> dict[str, Any] | None:
    """Retrieve a property check report by ID."""
    resp = await self._request(
        "GET",
        f"/api/properties/check/{url_quote(report_id, safe='')}",
        allow_404=True,
        operation="Property check report",
    )
    if resp is None:
        return None
    return cast(dict[str, Any], resp.json())

Retrieve a property check report by ID.

async def get_publishers_for_agent(self,
agent_url: str,
*,
since: str | None = None,
cursor: str | None = None,
status: str | None = None,
include: str | None = None,
limit: int | None = None,
legacy_api_prefix: bool = False) ‑> dict[str, typing.Any]
Expand source code
async def get_publishers_for_agent(
    self,
    agent_url: str,
    *,
    since: str | None = None,
    cursor: str | None = None,
    status: str | None = None,
    include: str | None = None,
    limit: int | None = None,
    legacy_api_prefix: bool = False,
) -> dict[str, Any]:
    """List publishers associated with an agent URL."""
    encoded = url_quote(agent_url, safe="")
    params = {
        k: v
        for k, v in {
            "since": since,
            "cursor": cursor,
            "status": status,
            "include": include,
            "limit": limit,
        }.items()
        if v is not None
    }
    prefix = "/api/v1" if legacy_api_prefix else "/v1"
    return await self._request_json(
        "GET",
        f"{prefix}/agents/{encoded}/publishers",
        params=params,
        operation="Publishers for agent",
    )

List publishers associated with an agent URL.

async def get_registry_stats(self) ‑> dict[str, typing.Any]
Expand source code
async def get_registry_stats(self) -> dict[str, Any]:
    """Get aggregate registry statistics."""
    resp = await self._request_ok(
        "GET",
        "/api/registry/stats",
        operation="Registry stats",
    )

    return cast(dict[str, Any], resp.json())

Get aggregate registry statistics.

async def get_storyboard(self, storyboard_id: str, *, compliance_target: str | None = None) ‑> dict[str, typing.Any]
Expand source code
async def get_storyboard(
    self,
    storyboard_id: str,
    *,
    compliance_target: str | None = None,
) -> dict[str, Any]:
    """Fetch one registry compliance storyboard."""
    params = {"compliance_target": compliance_target} if compliance_target is not None else None
    return await self._request_json(
        "GET",
        f"/api/storyboards/{url_quote(storyboard_id, safe='')}",
        params=params,
        operation="Storyboard get",
    )

Fetch one registry compliance storyboard.

async def get_storyboard_first_step(self, storyboard_id: str, *, compliance_target: str | None = None) ‑> dict[str, typing.Any]
Expand source code
async def get_storyboard_first_step(
    self,
    storyboard_id: str,
    *,
    compliance_target: str | None = None,
) -> dict[str, Any]:
    """Fetch the first runnable step for a storyboard."""
    params = {"compliance_target": compliance_target} if compliance_target is not None else None
    return await self._request_json(
        "GET",
        f"/api/storyboards/{url_quote(storyboard_id, safe='')}/first-step",
        params=params,
        operation="Storyboard first step",
    )

Fetch the first runnable step for a storyboard.

async def list_agents(self,
*,
type: str | None = None,
health: bool = False,
capabilities: bool = False,
properties: bool = False,
compliance: bool = False,
metric_id: str | list[str] | None = None,
accreditation: str | list[str] | None = None,
q: str | None = None,
verification_mode: str | list[str] | None = None,
verified: bool = False) ‑> list[FederatedAgentWithDetails]
Expand source code
async def list_agents(
    self,
    *,
    type: str | None = None,
    health: bool = False,
    capabilities: bool = False,
    properties: bool = False,
    compliance: bool = False,
    metric_id: str | list[str] | None = None,
    accreditation: str | list[str] | None = None,
    q: str | None = None,
    verification_mode: str | list[str] | None = None,
    verified: bool = False,
) -> list[FederatedAgentWithDetails]:
    """List registered and discovered agents.

    Measurement filters (``metric_id``, ``accreditation``, ``q``) imply
    ``type=measurement`` on the registry when ``type`` is omitted.
    """
    params: dict[str, Any] = {}
    if type is not None:
        params["type"] = type
    if health:
        params["health"] = "true"
    if capabilities:
        params["capabilities"] = "true"
    if properties:
        params["properties"] = "true"
    if compliance:
        params["compliance"] = "true"
    if metric_id is not None:
        params["metric_id"] = metric_id
    if accreditation is not None:
        params["accreditation"] = accreditation
    if q is not None:
        params["q"] = q
    if verification_mode is not None:
        params["verification_mode"] = verification_mode
    if verified:
        params["verified"] = "true"
    resp = await self._request_ok(
        "GET",
        "/api/registry/agents",
        params=params,
        operation="Agent list",
    )

    data = resp.json()
    return [
        self._parse(FederatedAgentWithDetails, a, "Agent list") for a in data.get("agents", [])
    ]

List registered and discovered agents.

Measurement filters (metric_id, accreditation, q) imply type=measurement on the registry when type is omitted.

async def list_brands(self, search: str | None = None, limit: int = 100, offset: int = 0) ‑> list[BrandRegistryItem]
Expand source code
async def list_brands(
    self,
    search: str | None = None,
    limit: int = 100,
    offset: int = 0,
) -> list[BrandRegistryItem]:
    """List brands in the registry."""
    params: dict[str, Any] = {"limit": limit, "offset": offset}
    if search is not None:
        params["search"] = search
    resp = await self._request_ok(
        "GET",
        "/api/brands/registry",
        params=params,
        operation="Brand list",
    )

    data = resp.json()
    return [self._parse(BrandRegistryItem, b, "Brand list") for b in data.get("brands", [])]

List brands in the registry.

async def list_community_mirror_adagents(self, *, limit: int | None = None, offset: int | None = None) ‑> CommunityMirrorListResponse
Expand source code
async def list_community_mirror_adagents(
    self,
    *,
    limit: int | None = None,
    offset: int | None = None,
) -> CommunityMirrorListResponse:
    """List published community mirror catalogs with their current etags.

    Fetches ``GET /api/registry/mirrors``. The list projection includes
    presence and freshness metadata but omits the full ``adagents_json``
    body; fetch a platform-specific mirror for the full document.

    Args:
        limit: Optional page size. The service defaults to 100 and clamps
            values to the 1-500 range.
        offset: Optional zero-based result offset. The service defaults to 0
            and clamps negative values to 0.

    Returns:
        The list response (``mirrors`` summaries plus ``total``).
    """
    params: dict[str, Any] = {}
    if limit is not None:
        params["limit"] = limit
    if offset is not None:
        params["offset"] = offset
    resp = await self._request_ok(
        "GET",
        "/api/registry/mirrors",
        params=params or None,
        operation="Community mirror list",
    )
    return self._parse(CommunityMirrorListResponse, resp.json(), "Community mirror list")

List published community mirror catalogs with their current etags.

Fetches GET /api/registry/mirrors. The list projection includes presence and freshness metadata but omits the full adagents_json body; fetch a platform-specific mirror for the full document.

Args
-----=
limit
Optional page size. The service defaults to 100 and clamps values to the 1-500 range.
offset
Optional zero-based result offset. The service defaults to 0 and clamps negative values to 0.

Returns -----= The list response (mirrors summaries plus total).

async def list_member_agents(self, *, auth_token: str, org: str | None = None) ‑> dict[str, typing.Any]
Expand source code
async def list_member_agents(
    self,
    *,
    auth_token: str,
    org: str | None = None,
) -> dict[str, Any]:
    """List the authenticated member's registered agents."""
    params = {"org": org} if org is not None else None
    return await self._request_json(
        "GET",
        "/api/me/agents",
        params=params,
        auth_token=auth_token,
        operation="Member agent list",
    )

List the authenticated member's registered agents.

async def list_members(self, limit: int = 100) ‑> list[Member]
Expand source code
async def list_members(self, limit: int = 100) -> list[Member]:
    """List organizations registered in the AAO member directory.

    Args:
        limit: Maximum number of members to return.

    Returns:
        List of Member objects.

    Raises:
        RegistryError: On HTTP or parsing errors.
    """
    if limit < 1:
        raise ValueError(f"limit must be at least 1, got {limit}")

    client = await self._get_client()
    try:
        response = await client.get(
            f"{self._base_url}/api/members",
            params={"limit": limit},
            headers={"User-Agent": self._user_agent},
            timeout=self._timeout,
        )
        if response.status_code != 200:
            raise _registry_http_error(
                response,
                method="GET",
                operation="Member list",
            )
        data = response.json()
        return [Member.model_validate(m) for m in data.get("members", [])]
    except RegistryError:
        raise
    except httpx.TimeoutException as e:
        raise RegistryError(f"Member list timed out after {self._timeout}s") from e
    except httpx.HTTPError as e:
        raise RegistryError(f"Member list failed: {e}") from e
    except (ValidationError, ValueError) as e:
        raise RegistryError(f"Member list failed: invalid response: {e}") from e

List organizations registered in the AAO member directory.

Args
-----=
limit
Maximum number of members to return.

Returns -----= List of Member objects.

Raises
-----=
RegistryError
On HTTP or parsing errors.
async def list_policies(self,
search: str | None = None,
category: str | None = None,
enforcement: str | None = None,
jurisdiction: str | None = None,
vertical: str | None = None,
domain: str | None = None,
limit: int = 20,
offset: int = 0) ‑> list[PolicySummary]
Expand source code
async def list_policies(
    self,
    search: str | None = None,
    category: str | None = None,
    enforcement: str | None = None,
    jurisdiction: str | None = None,
    vertical: str | None = None,
    domain: str | None = None,
    limit: int = 20,
    offset: int = 0,
) -> list[PolicySummary]:
    """List governance policies with optional filtering.

    Args:
        search: Full-text search on policy name and description.
        category: Filter by category ("regulation" or "standard").
        enforcement: Filter by enforcement level ("must", "should", "may").
        jurisdiction: Filter by jurisdiction with region alias matching.
        vertical: Filter by industry vertical.
        domain: Filter by governance domain ("campaign", "creative", etc.).
        limit: Results per page (default 20, max 1000).
        offset: Pagination offset.

    Returns:
        List of PolicySummary objects.

    Raises:
        RegistryError: On HTTP or parsing errors.
    """
    client = await self._get_client()
    params: dict[str, str | int] = {"limit": limit, "offset": offset}
    if search is not None:
        params["search"] = search
    if category is not None:
        params["category"] = category
    if enforcement is not None:
        params["enforcement"] = enforcement
    if jurisdiction is not None:
        params["jurisdiction"] = jurisdiction
    if vertical is not None:
        params["vertical"] = vertical
    if domain is not None:
        params["domain"] = domain

    try:
        response = await client.get(
            f"{self._base_url}/api/policies/registry",
            params=params,
            headers={"User-Agent": self._user_agent},
            timeout=self._timeout,
        )
        if response.status_code != 200:
            raise _registry_http_error(
                response,
                method="GET",
                operation="Policy list",
            )
        data = response.json()
        return [PolicySummary.model_validate(p) for p in data.get("policies", [])]
    except RegistryError:
        raise
    except httpx.TimeoutException as e:
        raise RegistryError(f"Policy list timed out after {self._timeout}s") from e
    except httpx.HTTPError as e:
        raise RegistryError(f"Policy list failed: {e}") from e
    except (ValidationError, ValueError) as e:
        raise RegistryError(f"Policy list failed: invalid response: {e}") from e

List governance policies with optional filtering.

Args
-----=
search
Full-text search on policy name and description.
category
Filter by category ("regulation" or "standard").
enforcement
Filter by enforcement level ("must", "should", "may").
jurisdiction
Filter by jurisdiction with region alias matching.
vertical
Filter by industry vertical.
domain
Filter by governance domain ("campaign", "creative", etc.).
limit
Results per page (default 20, max 1000).
offset
Pagination offset.

Returns -----= List of PolicySummary objects.

Raises
-----=
RegistryError
On HTTP or parsing errors.
async def list_properties(self, search: str | None = None, limit: int = 100, offset: int = 0) ‑> list[PropertyRegistryItem]
Expand source code
async def list_properties(
    self,
    search: str | None = None,
    limit: int = 100,
    offset: int = 0,
) -> list[PropertyRegistryItem]:
    """List properties in the registry."""
    params: dict[str, Any] = {"limit": limit, "offset": offset}
    if search is not None:
        params["search"] = search
    resp = await self._request_ok(
        "GET",
        "/api/properties/registry",
        params=params,
        operation="Property list",
    )

    data = resp.json()
    return [
        self._parse(PropertyRegistryItem, p, "Property list")
        for p in data.get("properties", [])
    ]

List properties in the registry.

async def list_publishers(self) ‑> list[FederatedPublisher]
Expand source code
async def list_publishers(self) -> list[FederatedPublisher]:
    """List publishers in the registry."""
    resp = await self._request_ok(
        "GET",
        "/api/registry/publishers",
        operation="Publisher list",
    )

    data = resp.json()
    return [
        self._parse(FederatedPublisher, p, "Publisher list") for p in data.get("publishers", [])
    ]

List publishers in the registry.

async def list_storyboards(self, *, category: str | None = None, compliance_target: str | None = None) ‑> dict[str, typing.Any]
Expand source code
async def list_storyboards(
    self,
    *,
    category: str | None = None,
    compliance_target: str | None = None,
) -> dict[str, Any]:
    """List available registry compliance storyboards."""
    params = {
        k: v
        for k, v in {"category": category, "compliance_target": compliance_target}.items()
        if v is not None
    }
    return await self._request_json(
        "GET",
        "/api/storyboards",
        params=params,
        operation="Storyboard list",
    )

List available registry compliance storyboards.

async def lookup_brand(self, domain: str, *, fresh: bool = False) ‑> ResolvedBrand | None
Expand source code
async def lookup_brand(self, domain: str, *, fresh: bool = False) -> ResolvedBrand | None:
    """Resolve a domain to its brand identity.

    Works for any domain — brand houses, sub-brands, and operators
    (agencies, DSPs) are all brands in the registry.

    Args:
        domain: Domain to resolve (e.g., "nike.com", "wpp.com").
        fresh: Request a live origin check instead of a cached registry
            result. Defaults to False. Use for authorization decisions
            that require current brand relationship evidence.

    Returns:
        ResolvedBrand if found, None if not in the registry.

    Raises:
        RegistryError: On HTTP or parsing errors.

    Example:
        brand = await registry.lookup_brand(request.brand.domain)
    """
    try:
        params = {"domain": domain}
        if fresh:
            params["fresh"] = "true"
        response = await self._request(
            "GET",
            "/api/brands/resolve",
            params=params,
            operation="Brand lookup",
            allow_404=True,
        )
        if response is None:
            return None
        data = response.json()
        if data is None:
            return None
        return ResolvedBrand.model_validate(data)
    except RegistryError:
        raise
    except httpx.TimeoutException as e:
        raise RegistryError(f"Brand lookup timed out after {self._timeout}s") from e
    except httpx.HTTPError as e:
        raise RegistryError(f"Brand lookup failed: {e}") from e
    except (ValidationError, ValueError) as e:
        raise RegistryError(f"Brand lookup failed: invalid response: {e}") from e

Resolve a domain to its brand identity.

Works for any domain — brand houses, sub-brands, and operators (agencies, DSPs) are all brands in the registry.

Args
-----=
domain
Domain to resolve (e.g., "nike.com", "wpp.com").
fresh
Request a live origin check instead of a cached registry result. Defaults to False. Use for authorization decisions that require current brand relationship evidence.

Returns -----= ResolvedBrand if found, None if not in the registry.

Raises
-----=
RegistryError
On HTTP or parsing errors.

Example -----= brand = await registry.lookup_brand(request.brand.domain)

async def lookup_brands(self, domains: list[str]) ‑> dict[str, ResolvedBrand | None]
Expand source code
async def lookup_brands(self, domains: list[str]) -> dict[str, ResolvedBrand | None]:
    """Bulk resolve domains to brand identities.

    Automatically chunks requests exceeding 100 domains.

    Args:
        domains: List of domains to resolve.

    Returns:
        Dict mapping each domain to its ResolvedBrand, or None if not found.

    Raises:
        RegistryError: On HTTP or parsing errors.
    """
    if not domains:
        return {}

    chunks = [
        domains[i : i + MAX_BULK_DOMAINS] for i in range(0, len(domains), MAX_BULK_DOMAINS)
    ]

    chunk_results = await asyncio.gather(
        *[self._lookup_brands_chunk(chunk) for chunk in chunks]
    )

    merged: dict[str, ResolvedBrand | None] = {}
    for result in chunk_results:
        merged.update(result)
    return merged

Bulk resolve domains to brand identities.

Automatically chunks requests exceeding 100 domains.

Args
-----=
domains
List of domains to resolve.

Returns -----= Dict mapping each domain to its ResolvedBrand, or None if not found.

Raises
-----=
RegistryError
On HTTP or parsing errors.
async def lookup_domain(self, domain: str) ‑> DomainLookupResult
Expand source code
async def lookup_domain(self, domain: str) -> DomainLookupResult:
    """Find all agents authorized for a publisher domain."""
    resp = await self._request_ok(
        "GET",
        f"/api/registry/lookup/domain/{url_quote(domain, safe='')}",
        operation="Domain lookup",
    )

    return self._parse(DomainLookupResult, resp.json(), "Domain lookup")

Find all agents authorized for a publisher domain.

async def lookup_manifest_ref(self, domain: str, *, type: str | None = None) ‑> dict[str, typing.Any]
Expand source code
async def lookup_manifest_ref(self, domain: str, *, type: str | None = None) -> dict[str, Any]:
    """Find the best manifest reference for a domain."""
    params: dict[str, Any] = {"domain": domain}
    if type is not None:
        params["type"] = type
    resp = await self._request_ok(
        "GET",
        "/api/manifest-refs/lookup",
        params=params,
        operation="Manifest ref lookup",
    )

    return cast(dict[str, Any], resp.json())

Find the best manifest reference for a domain.

async def lookup_operator(self, domain: str, *, scope: str | None = None) ‑> dict[str, typing.Any]
Expand source code
async def lookup_operator(
    self,
    domain: str,
    *,
    scope: str | None = None,
) -> dict[str, Any]:
    """Resolve registry operator metadata for a domain."""
    params: dict[str, Any] = {"domain": domain}
    if scope is not None:
        params["scope"] = scope
    return await self._request_json(
        "GET",
        "/api/registry/operator",
        params=params,
        operation="Operator lookup",
    )

Resolve registry operator metadata for a domain.

async def lookup_properties(self, domains: list[str]) ‑> dict[str, ResolvedProperty | None]
Expand source code
async def lookup_properties(self, domains: list[str]) -> dict[str, ResolvedProperty | None]:
    """Bulk resolve publisher domains to property info.

    Automatically chunks requests exceeding 100 domains.

    Args:
        domains: List of publisher domains to resolve.

    Returns:
        Dict mapping each domain to its ResolvedProperty, or None if not found.

    Raises:
        RegistryError: On HTTP or parsing errors.
    """
    if not domains:
        return {}

    chunks = [
        domains[i : i + MAX_BULK_DOMAINS] for i in range(0, len(domains), MAX_BULK_DOMAINS)
    ]

    chunk_results = await asyncio.gather(
        *[self._lookup_properties_chunk(chunk) for chunk in chunks]
    )

    merged: dict[str, ResolvedProperty | None] = {}
    for result in chunk_results:
        merged.update(result)
    return merged

Bulk resolve publisher domains to property info.

Automatically chunks requests exceeding 100 domains.

Args
-----=
domains
List of publisher domains to resolve.

Returns -----= Dict mapping each domain to its ResolvedProperty, or None if not found.

Raises
-----=
RegistryError
On HTTP or parsing errors.
async def lookup_property(self, domain: str) ‑> ResolvedProperty | None
Expand source code
async def lookup_property(self, domain: str) -> ResolvedProperty | None:
    """Resolve a publisher domain to its property info.

    Args:
        domain: Publisher domain to resolve (e.g., "nytimes.com").

    Returns:
        ResolvedProperty if found, None if the domain is not in the registry.

    Raises:
        RegistryError: On HTTP or parsing errors.
    """
    client = await self._get_client()
    try:
        response = await client.get(
            f"{self._base_url}/api/properties/resolve",
            params={"domain": domain},
            headers={"User-Agent": self._user_agent},
            timeout=self._timeout,
        )
        if response.status_code == 404:
            return None
        if response.status_code != 200:
            raise _registry_http_error(
                response,
                method="GET",
                operation="Property lookup",
            )
        data = response.json()
        if data is None:
            return None
        return ResolvedProperty.model_validate(data)
    except RegistryError:
        raise
    except httpx.TimeoutException as e:
        raise RegistryError(f"Property lookup timed out after {self._timeout}s") from e
    except httpx.HTTPError as e:
        raise RegistryError(f"Property lookup failed: {e}") from e
    except (ValidationError, ValueError) as e:
        raise RegistryError(f"Property lookup failed: invalid response: {e}") from e

Resolve a publisher domain to its property info.

Args
-----=
domain
Publisher domain to resolve (e.g., "nytimes.com").

Returns -----= ResolvedProperty if found, None if the domain is not in the registry.

Raises
-----=
RegistryError
On HTTP or parsing errors.
async def lookup_property_identifier(self, type: str, value: str) ‑> dict[str, typing.Any]
Expand source code
async def lookup_property_identifier(self, type: str, value: str) -> dict[str, Any]:
    """Find agents holding a specific property identifier."""
    resp = await self._request_ok(
        "GET",
        "/api/registry/lookup/property",
        params={"type": type, "value": value},
        operation="Property identifier lookup",
    )

    return cast(dict[str, Any], resp.json())

Find agents holding a specific property identifier.

async def lookup_publisher(self, domain: str) ‑> dict[str, typing.Any]
Expand source code
async def lookup_publisher(self, domain: str) -> dict[str, Any]:
    """Resolve registry publisher metadata for a domain."""
    return await self._request_json(
        "GET",
        "/api/registry/publisher",
        params={"domain": domain},
        operation="Publisher lookup",
    )

Resolve registry publisher metadata for a domain.

async def lookup_publisher_agent_authorization(self, domain: str, agent: str) ‑> dict[str, typing.Any]
Expand source code
async def lookup_publisher_agent_authorization(
    self,
    domain: str,
    agent: str,
) -> dict[str, Any]:
    """Resolve whether a publisher authorizes an agent."""
    return await self._request_json(
        "GET",
        "/api/registry/publisher/authorization",
        params={"domain": domain, "agent": agent},
        operation="Publisher agent authorization lookup",
    )

Resolve whether a publisher authorizes an agent.

async def policy_history(self, policy_id: str, limit: int = 20, offset: int = 0) ‑> PolicyHistory | None
Expand source code
async def policy_history(
    self,
    policy_id: str,
    limit: int = 20,
    offset: int = 0,
) -> PolicyHistory | None:
    """Retrieve edit history for a policy.

    Args:
        policy_id: Policy identifier.
        limit: Maximum revisions to return (default 20, max 100).
        offset: Pagination offset.

    Returns:
        PolicyHistory if found, None if the policy doesn't exist.

    Raises:
        RegistryError: On HTTP or parsing errors.
    """
    client = await self._get_client()
    try:
        response = await client.get(
            f"{self._base_url}/api/policies/history",
            params={"policy_id": policy_id, "limit": limit, "offset": offset},
            headers={"User-Agent": self._user_agent},
            timeout=self._timeout,
        )
        if response.status_code == 404:
            return None
        if response.status_code != 200:
            raise _registry_http_error(
                response,
                method="GET",
                operation="Policy history",
            )
        data = response.json()
        if data is None:
            return None
        return PolicyHistory.model_validate(data)
    except RegistryError:
        raise
    except httpx.TimeoutException as e:
        raise RegistryError(f"Policy history timed out after {self._timeout}s") from e
    except httpx.HTTPError as e:
        raise RegistryError(f"Policy history failed: {e}") from e
    except (ValidationError, ValueError) as e:
        raise RegistryError(f"Policy history failed: invalid response: {e}") from e

Retrieve edit history for a policy.

Args
-----=
policy_id
Policy identifier.
limit
Maximum revisions to return (default 20, max 100).
offset
Pagination offset.

Returns -----= PolicyHistory if found, None if the policy doesn't exist.

Raises
-----=
RegistryError
On HTTP or parsing errors.
async def property_history(self, domain: str, limit: int = 20, offset: int = 0) ‑> PropertyActivity | None
Expand source code
async def property_history(
    self,
    domain: str,
    limit: int = 20,
    offset: int = 0,
) -> PropertyActivity | None:
    """Get edit history for a property."""
    resp = await self._request(
        "GET",
        "/api/properties/history",
        params={"domain": domain, "limit": limit, "offset": offset},
        allow_404=True,
        operation="Property history",
    )
    if resp is None:
        return None
    return self._parse(PropertyActivity, resp.json(), "Property history")

Get edit history for a property.

async def publish_community_mirror_adagents(self, platform: str, config: dict[str, Any], *, auth_token: str) ‑> CommunityMirrorPublishResponse
Expand source code
async def publish_community_mirror_adagents(
    self,
    platform: str,
    config: dict[str, Any],
    *,
    auth_token: str,
) -> CommunityMirrorPublishResponse:
    """Publish or update a catalog-only community mirror adagents.json descriptor.

    Persists the mirror under ``PUT /api/registry/mirrors/{platform}``. Use
    ``create_adagents`` (the generator endpoint) when you only need to
    validate or preview the document without saving it. The publish body is
    catalog-only; the service forces ``authorized_agents: []``.

    Args:
        platform: Stable platform key. Trimmed/lowercased and validated
            against ``^[a-z0-9_-]{1,64}$``.
        config: Catalog config (see ``build_community_mirror_adagents``).
            Any ``properties[].platform`` values must match ``platform``.
        auth_token: Bearer token required for save operations.

    Returns:
        The publish response.

    Raises:
        RegistryError: On platform/catalog validation or HTTP errors.
    """
    normalized_platform = _normalize_community_mirror_platform(platform)
    catalog = build_community_mirror_adagents(config)
    self._assert_community_mirror_properties_match_platform(normalized_platform, catalog)
    resp = await self._request_ok(
        "PUT",
        f"/api/registry/mirrors/{url_quote(normalized_platform, safe='')}",
        json_body=catalog,
        auth_token=auth_token,
        operation="Community mirror publish",
    )
    return self._parse(CommunityMirrorPublishResponse, resp.json(), "Community mirror publish")

Publish or update a catalog-only community mirror adagents.json descriptor.

Persists the mirror under PUT /api/registry/mirrors/{platform}. Use create_adagents (the generator endpoint) when you only need to validate or preview the document without saving it. The publish body is catalog-only; the service forces authorized_agents: [].

Args
-----=
platform
Stable platform key. Trimmed/lowercased and validated against ^[a-z0-9_-]{1,64}$.
config
Catalog config (see build_community_mirror_adagents). Any properties[].platform values must match platform.
auth_token
Bearer token required for save operations.

Returns -----= The publish response.

Raises
-----=
RegistryError
On platform/catalog validation or HTTP errors.
async def refresh_agent(self, agent_url: str, *, auth_token: str) ‑> dict[str, typing.Any]
Expand source code
async def refresh_agent(
    self,
    agent_url: str,
    *,
    auth_token: str,
) -> dict[str, Any]:
    """Refresh an agent's registry health/capability/compliance snapshot."""
    encoded = self._encoded_agent_url(agent_url)
    return await self._request_json(
        "POST",
        f"/api/registry/agents/{encoded}/refresh",
        auth_token=auth_token,
        operation="Agent refresh",
        expected_status={200, 202},
    )

Refresh an agent's registry health/capability/compliance snapshot.

async def register_member_agent(self, *, auth_token: str, org: str | None = None, **body: Any) ‑> dict[str, typing.Any]
Expand source code
async def register_member_agent(
    self,
    *,
    auth_token: str,
    org: str | None = None,
    **body: Any,
) -> dict[str, Any]:
    """Register an agent for the authenticated member."""
    params = {"org": org} if org is not None else None
    return await self._request_json(
        "POST",
        "/api/me/agents",
        params=params,
        json_body=dict(body),
        auth_token=auth_token,
        operation="Member agent register",
        expected_status={200, 201},
    )

Register an agent for the authenticated member.

async def remove_member_agent(self, url: str, *, auth_token: str, org: str | None = None) ‑> dict[str, typing.Any]
Expand source code
async def remove_member_agent(
    self,
    url: str,
    *,
    auth_token: str,
    org: str | None = None,
) -> dict[str, Any]:
    """Remove one member-owned agent."""
    params = {"org": org} if org is not None else None
    return await self._request_json(
        "DELETE",
        f"/api/me/agents/{url_quote(url, safe='')}",
        params=params,
        auth_token=auth_token,
        operation="Member agent remove",
    )

Remove one member-owned agent.

async def request_brand_crawl(self, *, auth_token: str, **body: Any) ‑> dict[str, typing.Any]
Expand source code
async def request_brand_crawl(
    self,
    *,
    auth_token: str,
    **body: Any,
) -> dict[str, Any]:
    """Request a brand crawl through the registry."""
    return await self._request_json(
        "POST",
        "/api/registry/brand-crawl-request",
        json_body=dict(body),
        auth_token=auth_token,
        operation="Brand crawl request",
        expected_status={200, 202},
    )

Request a brand crawl through the registry.

async def request_crawl(self, domain: str, *, auth_token: str) ‑> dict[str, typing.Any]
Expand source code
async def request_crawl(self, domain: str, *, auth_token: str) -> dict[str, Any]:
    """Request a domain re-crawl (auth required)."""
    resp = await self._request_ok(
        "POST",
        "/api/registry/crawl-request",
        json_body={"domain": domain},
        auth_token=auth_token,
        operation="Crawl request",
        expected_status={200, 202},
    )

    return cast(dict[str, Any], resp.json())

Request a domain re-crawl (auth required).

async def request_manager_revalidation(self, *, auth_token: str, **body: Any) ‑> dict[str, typing.Any]
Expand source code
async def request_manager_revalidation(
    self,
    *,
    auth_token: str,
    **body: Any,
) -> dict[str, Any]:
    """Request manager revalidation for registry-managed data."""
    return await self._request_json(
        "POST",
        "/api/registry/manager-revalidation-request",
        json_body=dict(body),
        auth_token=auth_token,
        operation="Manager revalidation request",
        expected_status={200, 202},
    )

Request manager revalidation for registry-managed data.

async def requeue_agent_for_heartbeat(self, agent_url: str, *, auth_token: str) ‑> dict[str, typing.Any]
Expand source code
async def requeue_agent_for_heartbeat(
    self,
    agent_url: str,
    *,
    auth_token: str,
) -> dict[str, Any]:
    """Requeue an agent for heartbeat monitoring."""
    encoded = self._encoded_agent_url(agent_url)
    return await self._request_json(
        "POST",
        f"/api/registry/agents/{encoded}/monitoring/requeue",
        auth_token=auth_token,
        operation="Agent heartbeat requeue",
    )

Requeue an agent for heartbeat monitoring.

async def resolve_policies(self, policy_ids: list[str]) ‑> dict[str, Policy | None]
Expand source code
async def resolve_policies(
    self,
    policy_ids: list[str],
) -> dict[str, Policy | None]:
    """Bulk resolve policies by ID.

    Automatically chunks requests exceeding 100 policy IDs.

    Args:
        policy_ids: List of policy identifiers to resolve.

    Returns:
        Dict mapping each policy_id to its Policy, or None if not found.

    Raises:
        RegistryError: On HTTP or parsing errors.
    """
    if not policy_ids:
        return {}

    chunks = [
        policy_ids[i : i + MAX_BULK_POLICIES]
        for i in range(0, len(policy_ids), MAX_BULK_POLICIES)
    ]

    chunk_results = await asyncio.gather(
        *[self._resolve_policies_chunk(chunk) for chunk in chunks]
    )

    merged: dict[str, Policy | None] = {}
    for result in chunk_results:
        merged.update(result)
    return merged

Bulk resolve policies by ID.

Automatically chunks requests exceeding 100 policy IDs.

Args
-----=
policy_ids
List of policy identifiers to resolve.

Returns -----= Dict mapping each policy_id to its Policy, or None if not found.

Raises
-----=
RegistryError
On HTTP or parsing errors.
async def resolve_policy(self, policy_id: str, version: str | None = None) ‑> Policy | None
Expand source code
async def resolve_policy(
    self,
    policy_id: str,
    version: str | None = None,
) -> Policy | None:
    """Resolve a single policy by ID.

    Args:
        policy_id: Policy identifier (e.g., "gdpr_consent").
        version: Optional version pin; returns None if current version differs.

    Returns:
        Policy if found, None if not in the registry.

    Raises:
        RegistryError: On HTTP or parsing errors.
    """
    client = await self._get_client()
    params: dict[str, str] = {"policy_id": policy_id}
    if version is not None:
        params["version"] = version

    try:
        response = await client.get(
            f"{self._base_url}/api/policies/resolve",
            params=params,
            headers={"User-Agent": self._user_agent},
            timeout=self._timeout,
        )
        if response.status_code == 404:
            return None
        if response.status_code != 200:
            raise _registry_http_error(
                response,
                method="GET",
                operation="Policy resolve",
            )
        data = response.json()
        if data is None:
            return None
        return Policy.model_validate(data)
    except RegistryError:
        raise
    except httpx.TimeoutException as e:
        raise RegistryError(f"Policy resolve timed out after {self._timeout}s") from e
    except httpx.HTTPError as e:
        raise RegistryError(f"Policy resolve failed: {e}") from e
    except (ValidationError, ValueError) as e:
        raise RegistryError(f"Policy resolve failed: invalid response: {e}") from e

Resolve a single policy by ID.

Args
-----=
policy_id
Policy identifier (e.g., "gdpr_consent").
version
Optional version pin; returns None if current version differs.

Returns -----= Policy if found, None if not in the registry.

Raises
-----=
RegistryError
On HTTP or parsing errors.
async def run_storyboard(self, agent_url: str, storyboard_id: str, *, auth_token: str) ‑> dict[str, typing.Any]
Expand source code
async def run_storyboard(
    self,
    agent_url: str,
    storyboard_id: str,
    *,
    auth_token: str,
) -> dict[str, Any]:
    """Run a storyboard against an agent."""
    encoded = self._encoded_agent_url(agent_url)
    return await self._request_json(
        "POST",
        f"/api/registry/agents/{encoded}/storyboard/{url_quote(storyboard_id, safe='')}/run",
        auth_token=auth_token,
        operation="Storyboard run",
        expected_status={200, 202},
    )

Run a storyboard against an agent.

async def run_storyboard_step(self, agent_url: str, storyboard_id: str, step_id: str, *, auth_token: str, **body: Any) ‑> dict[str, typing.Any]
Expand source code
async def run_storyboard_step(
    self,
    agent_url: str,
    storyboard_id: str,
    step_id: str,
    *,
    auth_token: str,
    **body: Any,
) -> dict[str, Any]:
    """Run one storyboard step against an agent."""
    encoded = self._encoded_agent_url(agent_url)
    return await self._request_json(
        "POST",
        "/api/registry/agents/"
        f"{encoded}/storyboard/{url_quote(storyboard_id, safe='')}/step/"
        f"{url_quote(step_id, safe='')}",
        json_body=dict(body),
        auth_token=auth_token,
        operation="Storyboard step run",
    )

Run one storyboard step against an agent.

async def save_agent_oauth_client_credentials(self, agent_url: str, *, auth_token: str, **body: Any) ‑> dict[str, typing.Any]
Expand source code
async def save_agent_oauth_client_credentials(
    self,
    agent_url: str,
    *,
    auth_token: str,
    **body: Any,
) -> dict[str, Any]:
    """Save OAuth client credentials for an agent."""
    encoded = self._encoded_agent_url(agent_url)
    return await self._request_json(
        "PUT",
        f"/api/registry/agents/{encoded}/oauth-client-credentials",
        json_body=dict(body),
        auth_token=auth_token,
        operation="Agent OAuth client credentials save",
    )

Save OAuth client credentials for an agent.

async def save_brand(self,
domain: str,
brand_name: str,
*,
auth_token: str,
brand_manifest: dict[str, Any] | None = None) ‑> dict[str, typing.Any]
Expand source code
async def save_brand(
    self,
    domain: str,
    brand_name: str,
    *,
    auth_token: str,
    brand_manifest: dict[str, Any] | None = None,
) -> dict[str, Any]:
    """Save or update a brand in the registry (auth required)."""
    body: dict[str, Any] = {"domain": domain, "brand_name": brand_name}
    if brand_manifest is not None:
        body["brand_manifest"] = brand_manifest
    resp = await self._request_ok(
        "POST",
        "/api/brands/save",
        json_body=body,
        auth_token=auth_token,
        operation="Brand save",
    )

    return cast(dict[str, Any], resp.json())

Save or update a brand in the registry (auth required).

async def save_policy(self,
policy_id: str,
version: str,
name: str,
category: str,
enforcement: str,
policy: str,
*,
auth_token: str,
description: str | None = None,
jurisdictions: list[str] | None = None,
region_aliases: dict[str, list[str]] | None = None,
verticals: list[str] | None = None,
channels: list[str] | None = None,
effective_date: str | None = None,
sunset_date: str | None = None,
governance_domains: list[str] | None = None,
source_url: str | None = None,
source_name: str | None = None,
guidance: str | None = None,
exemplars: dict[str, Any] | None = None,
ext: dict[str, Any] | None = None) ‑> dict[str, typing.Any]
Expand source code
async def save_policy(
    self,
    policy_id: str,
    version: str,
    name: str,
    category: str,
    enforcement: str,
    policy: str,
    *,
    auth_token: str,
    description: str | None = None,
    jurisdictions: list[str] | None = None,
    region_aliases: dict[str, list[str]] | None = None,
    verticals: list[str] | None = None,
    channels: list[str] | None = None,
    effective_date: str | None = None,
    sunset_date: str | None = None,
    governance_domains: list[str] | None = None,
    source_url: str | None = None,
    source_name: str | None = None,
    guidance: str | None = None,
    exemplars: dict[str, Any] | None = None,
    ext: dict[str, Any] | None = None,
) -> dict[str, Any]:
    """Create or update a community-contributed policy.

    Requires authentication. Cannot edit registry-sourced or pending policies.

    Args:
        policy_id: Policy identifier (lowercase alphanumeric with underscores).
        version: Semantic version string.
        name: Human-readable policy name.
        category: "regulation" or "standard".
        enforcement: "must", "should", or "may".
        policy: Natural language policy text.
        auth_token: API key for authentication.
        description: Policy description.
        jurisdictions: ISO jurisdiction codes.
        region_aliases: Region alias mappings (e.g., {"EU": ["DE", "FR"]}).
        verticals: Industry verticals.
        channels: Media channels.
        effective_date: ISO 8601 date when enforcement begins.
        sunset_date: ISO 8601 date when enforcement ends.
        governance_domains: Applicable domains ("campaign", "creative", etc.).
        source_url: URL of the source regulation/standard.
        source_name: Name of the source.
        guidance: Implementation guidance text.
        exemplars: Pass/fail calibration scenarios.
        ext: Extension data.

    Returns:
        Dict with success, message, policy_id, and revision_number.

    Raises:
        RegistryError: On HTTP or parsing errors (400, 401, 409, 429).
    """
    client = await self._get_client()
    body: dict[str, Any] = {
        "policy_id": policy_id,
        "version": version,
        "name": name,
        "category": category,
        "enforcement": enforcement,
        "policy": policy,
    }
    for key, value in [
        ("description", description),
        ("jurisdictions", jurisdictions),
        ("region_aliases", region_aliases),
        ("verticals", verticals),
        ("channels", channels),
        ("effective_date", effective_date),
        ("sunset_date", sunset_date),
        ("governance_domains", governance_domains),
        ("source_url", source_url),
        ("source_name", source_name),
        ("guidance", guidance),
        ("exemplars", exemplars),
        ("ext", ext),
    ]:
        if value is not None:
            body[key] = value

    try:
        response = await client.post(
            f"{self._base_url}/api/policies/save",
            json=body,
            headers={
                "User-Agent": self._user_agent,
                "Authorization": f"Bearer {auth_token}",
            },
            timeout=self._timeout,
        )
        if response.status_code != 200:
            raise _registry_http_error(
                response,
                method="POST",
                operation="Policy save",
            )
        result: dict[str, Any] = response.json()
        return result
    except RegistryError:
        raise
    except httpx.TimeoutException as e:
        raise RegistryError(f"Policy save timed out after {self._timeout}s") from e
    except httpx.HTTPError as e:
        raise RegistryError(f"Policy save failed: {e}") from e

Create or update a community-contributed policy.

Requires authentication. Cannot edit registry-sourced or pending policies.

Args
-----=
policy_id
Policy identifier (lowercase alphanumeric with underscores).
version
Semantic version string.
name
Human-readable policy name.
category
"regulation" or "standard".
enforcement
"must", "should", or "may".
policy
Natural language policy text.
auth_token
API key for authentication.
description
Policy description.
jurisdictions
ISO jurisdiction codes.
region_aliases
Region alias mappings (e.g., {"EU": ["DE", "FR"]}).
verticals
Industry verticals.
channels
Media channels.
effective_date
ISO 8601 date when enforcement begins.
sunset_date
ISO 8601 date when enforcement ends.
governance_domains
Applicable domains ("campaign", "creative", etc.).
source_url
URL of the source regulation/standard.
source_name
Name of the source.
guidance
Implementation guidance text.
exemplars
Pass/fail calibration scenarios.
ext
Extension data.

Returns -----= Dict with success, message, policy_id, and revision_number.

Raises
-----=
RegistryError
On HTTP or parsing errors (400, 401, 409, 429).
async def save_property(self,
publisher_domain: str,
authorized_agents: list[dict[str, Any]],
*,
auth_token: str,
properties: list[dict[str, Any]] | None = None,
contact: dict[str, str] | None = None) ‑> dict[str, typing.Any]
Expand source code
async def save_property(
    self,
    publisher_domain: str,
    authorized_agents: list[dict[str, Any]],
    *,
    auth_token: str,
    properties: list[dict[str, Any]] | None = None,
    contact: dict[str, str] | None = None,
) -> dict[str, Any]:
    """Save or update a hosted property (auth required)."""
    body: dict[str, Any] = {
        "publisher_domain": publisher_domain,
        "authorized_agents": authorized_agents,
    }
    if properties is not None:
        body["properties"] = properties
    if contact is not None:
        body["contact"] = contact
    resp = await self._request_ok(
        "POST",
        "/api/properties/save",
        json_body=body,
        auth_token=auth_token,
        operation="Property save",
    )

    return cast(dict[str, Any], resp.json())

Save or update a hosted property (auth required).

async def search(self, q: str) ‑> dict[str, typing.Any]
Expand source code
async def search(self, q: str) -> dict[str, Any]:
    """Search across brands, publishers, and properties."""
    resp = await self._request_ok(
        "GET",
        "/api/search",
        params={"q": q},
        operation="Search",
    )

    return cast(dict[str, Any], resp.json())

Search across brands, publishers, and properties.

async def search_agents(self,
*,
auth_token: str,
channels: str | None = None,
property_types: str | None = None,
markets: str | None = None,
categories: str | None = None,
tags: str | None = None,
delivery_types: str | None = None,
has_tmp: bool | None = None,
min_properties: int | None = None,
cursor: str | None = None,
limit: int = 50) ‑> dict[str, typing.Any]
Expand source code
async def search_agents(
    self,
    *,
    auth_token: str,
    channels: str | None = None,
    property_types: str | None = None,
    markets: str | None = None,
    categories: str | None = None,
    tags: str | None = None,
    delivery_types: str | None = None,
    has_tmp: bool | None = None,
    min_properties: int | None = None,
    cursor: str | None = None,
    limit: int = 50,
) -> dict[str, Any]:
    """Search agents by inventory profile (auth required)."""
    params: dict[str, Any] = {"limit": limit}
    for key, val in [
        ("channels", channels),
        ("property_types", property_types),
        ("markets", markets),
        ("categories", categories),
        ("tags", tags),
        ("delivery_types", delivery_types),
        ("cursor", cursor),
    ]:
        if val is not None:
            params[key] = val
    if has_tmp is not None:
        params["has_tmp"] = str(has_tmp).lower()
    if min_properties is not None:
        params["min_properties"] = min_properties
    resp = await self._request_ok(
        "GET",
        "/api/registry/agents/search",
        params=params,
        auth_token=auth_token,
        operation="Agent search",
    )

    return cast(dict[str, Any], resp.json())

Search agents by inventory profile (auth required).

async def setup_my_brand(self, *, auth_token: str, **body: Any) ‑> dict[str, typing.Any]
Expand source code
async def setup_my_brand(self, *, auth_token: str, **body: Any) -> dict[str, Any]:
    """Set up a brand record for the authenticated member."""
    return await self._request_json(
        "POST",
        "/api/brands/setup-my-brand",
        json_body=dict(body),
        auth_token=auth_token,
        operation="Brand setup",
    )

Set up a brand record for the authenticated member.

async def test_agent_oauth_client_credentials(self, agent_url: str, *, auth_token: str) ‑> dict[str, typing.Any]
Expand source code
async def test_agent_oauth_client_credentials(
    self,
    agent_url: str,
    *,
    auth_token: str,
) -> dict[str, Any]:
    """Test saved OAuth client credentials for an agent."""
    encoded = self._encoded_agent_url(agent_url)
    return await self._request_json(
        "POST",
        f"/api/registry/agents/{encoded}/oauth-client-credentials/test",
        auth_token=auth_token,
        operation="Agent OAuth client credentials test",
    )

Test saved OAuth client credentials for an agent.

async def update_agent_compliance_opt_out(self, agent_url: str, *, auth_token: str, **body: Any) ‑> dict[str, typing.Any]
Expand source code
async def update_agent_compliance_opt_out(
    self,
    agent_url: str,
    *,
    auth_token: str,
    **body: Any,
) -> dict[str, Any]:
    """Update agent compliance opt-out state."""
    encoded = self._encoded_agent_url(agent_url)
    return await self._request_json(
        "PUT",
        f"/api/registry/agents/{encoded}/compliance/opt-out",
        json_body=dict(body),
        auth_token=auth_token,
        operation="Agent compliance opt-out update",
    )

Update agent compliance opt-out state.

async def update_agent_lifecycle(self, agent_url: str, *, auth_token: str, **body: Any) ‑> dict[str, typing.Any]
Expand source code
async def update_agent_lifecycle(
    self,
    agent_url: str,
    *,
    auth_token: str,
    **body: Any,
) -> dict[str, Any]:
    """Update an agent lifecycle stage."""
    encoded = self._encoded_agent_url(agent_url)
    return await self._request_json(
        "PUT",
        f"/api/registry/agents/{encoded}/lifecycle",
        json_body=dict(body),
        auth_token=auth_token,
        operation="Agent lifecycle update",
    )

Update an agent lifecycle stage.

async def update_agent_monitoring_interval(self, agent_url: str, *, auth_token: str, **body: Any) ‑> dict[str, typing.Any]
Expand source code
async def update_agent_monitoring_interval(
    self,
    agent_url: str,
    *,
    auth_token: str,
    **body: Any,
) -> dict[str, Any]:
    """Update monitoring interval for an agent."""
    encoded = self._encoded_agent_url(agent_url)
    return await self._request_json(
        "PUT",
        f"/api/registry/agents/{encoded}/monitoring/interval",
        json_body=dict(body),
        auth_token=auth_token,
        operation="Agent monitoring interval update",
    )

Update monitoring interval for an agent.

async def update_agent_monitoring_pause(self, agent_url: str, *, auth_token: str, **body: Any) ‑> dict[str, typing.Any]
Expand source code
async def update_agent_monitoring_pause(
    self,
    agent_url: str,
    *,
    auth_token: str,
    **body: Any,
) -> dict[str, Any]:
    """Pause or resume monitoring for an agent."""
    encoded = self._encoded_agent_url(agent_url)
    return await self._request_json(
        "PUT",
        f"/api/registry/agents/{encoded}/monitoring/pause",
        json_body=dict(body),
        auth_token=auth_token,
        operation="Agent monitoring pause update",
    )

Pause or resume monitoring for an agent.

async def update_member_agent(self, url: str, *, auth_token: str, org: str | None = None, **body: Any) ‑> dict[str, typing.Any]
Expand source code
async def update_member_agent(
    self,
    url: str,
    *,
    auth_token: str,
    org: str | None = None,
    **body: Any,
) -> dict[str, Any]:
    """Update one member-owned agent."""
    params = {"org": org} if org is not None else None
    return await self._request_json(
        "PATCH",
        f"/api/me/agents/{url_quote(url, safe='')}",
        params=params,
        json_body=dict(body),
        auth_token=auth_token,
        operation="Member agent update",
    )

Update one member-owned agent.

async def upsert_community_mirror_adagents(self, config: dict[str, Any], *, platform: str | None = None, auth_token: str) ‑> CommunityMirrorPublishResponse
Expand source code
async def upsert_community_mirror_adagents(
    self,
    config: dict[str, Any],
    *,
    platform: str | None = None,
    auth_token: str,
) -> CommunityMirrorPublishResponse:
    """Publish or update a community mirror, inferring the platform key.

    The platform key is resolved from the ``platform`` argument, then
    ``config["platform"]``, then a single consistent ``properties[].platform``
    value. Ambiguous property platforms raise an error.

    Args:
        config: Catalog config (see ``build_community_mirror_adagents``).
        platform: Explicit platform key. Takes precedence over inference.
        auth_token: Bearer token required for save operations.

    Returns:
        The publish response.

    Raises:
        RegistryError: If a platform key cannot be resolved, property
            platforms are ambiguous, or on validation/HTTP errors.
    """
    resolved_platform = (
        platform
        if platform is not None
        else self._community_mirror_platform_from_config(config)
    )
    return await self.publish_community_mirror_adagents(
        resolved_platform, config, auth_token=auth_token
    )

Publish or update a community mirror, inferring the platform key.

The platform key is resolved from the platform argument, then config["platform"], then a single consistent properties[].platform value. Ambiguous property platforms raise an error.

Args
-----=
config
Catalog config (see build_community_mirror_adagents).
platform
Explicit platform key. Takes precedence over inference.
auth_token
Bearer token required for save operations.

Returns -----= The publish response.

Raises
-----=
RegistryError
If a platform key cannot be resolved, property platforms are ambiguous, or on validation/HTTP errors.
async def validate_adagents(self, domain: str) ‑> dict[str, typing.Any]
Expand source code
async def validate_adagents(self, domain: str) -> dict[str, Any]:
    """Validate a domain's adagents.json via the registry API."""
    resp = await self._request_ok(
        "POST",
        "/api/adagents/validate",
        json_body={"domain": domain},
        operation="Adagents validate",
    )

    return cast(dict[str, Any], resp.json())

Validate a domain's adagents.json via the registry API.

async def validate_product_authorization(self, agent_url: str, publisher_properties: list[dict[str, Any]]) ‑> dict[str, typing.Any]
Expand source code
async def validate_product_authorization(
    self,
    agent_url: str,
    publisher_properties: list[dict[str, Any]],
) -> dict[str, Any]:
    """Check whether an agent is authorized to sell products."""
    resp = await self._request_ok(
        "POST",
        "/api/registry/validate/product-authorization",
        json_body={
            "agent_url": agent_url,
            "publisher_properties": publisher_properties,
        },
        operation="Product authorization",
    )

    return cast(dict[str, Any], resp.json())

Check whether an agent is authorized to sell products.

async def validate_property(self, domain: str) ‑> ValidationResult
Expand source code
async def validate_property(self, domain: str) -> ValidationResult:
    """Validate a domain's adagents.json configuration."""
    resp = await self._request_ok(
        "GET",
        "/api/properties/validate",
        params={"domain": domain},
        operation="Property validate",
    )

    return self._parse(ValidationResult, resp.json(), "Property validate")

Validate a domain's adagents.json configuration.

async def validate_property_authorization(self, agent_url: str, identifier_type: str, identifier_value: str) ‑> dict[str, typing.Any]
Expand source code
async def validate_property_authorization(
    self,
    agent_url: str,
    identifier_type: str,
    identifier_value: str,
) -> dict[str, Any]:
    """Quick check if a property identifier is authorized for an agent."""
    resp = await self._request_ok(
        "GET",
        "/api/registry/validate/property-authorization",
        params={
            "agent_url": agent_url,
            "identifier_type": identifier_type,
            "identifier_value": identifier_value,
        },
        operation="Property authorization",
    )

    return cast(dict[str, Any], resp.json())

Quick check if a property identifier is authorized for an agent.

async def validate_publisher(self, domain: str) ‑> dict[str, typing.Any]
Expand source code
async def validate_publisher(self, domain: str) -> dict[str, Any]:
    """Validate a publisher domain's adagents.json and return stats."""
    resp = await self._request_ok(
        "GET",
        "/api/public/validate-publisher",
        params={"domain": domain},
        operation="Publisher validation",
    )

    return cast(dict[str, Any], resp.json())

Validate a publisher domain's adagents.json and return stats.

async def verify_hosted_property_origin(self, domain: str, *, auth_token: str | None = None) ‑> dict[str, typing.Any]
Expand source code
async def verify_hosted_property_origin(
    self,
    domain: str,
    *,
    auth_token: str | None = None,
) -> dict[str, Any]:
    """Verify a hosted property's origin adagents.json delegation."""
    return await self._request_json(
        "POST",
        f"/api/properties/hosted/{url_quote(domain, safe='')}/verify-origin",
        auth_token=auth_token,
        operation="Hosted property origin verification",
    )

Verify a hosted property's origin adagents.json delegation.

class RegistryError (message: str,
status_code: int | None = None,
*,
method: str | None = None,
retry_after_seconds: float | None = None,
details: dict[str, Any] | None = None)
Expand source code
class RegistryError(ADCPError):
    """Error from AdCP registry API operations (brand/property lookups)."""

    def __init__(
        self,
        message: str,
        status_code: int | None = None,
        *,
        method: str | None = None,
        retry_after_seconds: float | None = None,
        details: dict[str, Any] | None = None,
    ):
        """Initialize registry error."""
        self.status_code = status_code
        self.method = method
        self.retry_after_seconds = retry_after_seconds
        self.details = details
        suggestion = "Check that the registry API is accessible and the domain is valid."
        super().__init__(message, suggestion=suggestion)

Error from AdCP registry API operations (brand/property lookups).

Initialize registry error.

Ancestors

  • ADCPError
  • builtins.Exception
  • builtins.BaseException

Inherited members

class RegistrySync (client: RegistryClient,
*,
auth_token: str,
poll_interval: float = 60.0,
cursor_store: CursorStore | None = None,
types: str | None = None,
batch_size: int = 100)
Expand source code
class RegistrySync:
    """Polls the registry change feed and dispatches events to handlers.

    Args:
        client: RegistryClient instance for HTTP calls.
        auth_token: Bearer token for feed access.
        poll_interval: Seconds between polls (default 60).
        cursor_store: Optional CursorStore for persistence.
            Defaults to FileCursorStore.
        types: Optional event type filter (e.g., "property.*,agent.*").
        batch_size: Max events per poll (default 100, max 10000).
    """

    def __init__(
        self,
        client: RegistryClient,
        *,
        auth_token: str,
        poll_interval: float = 60.0,
        cursor_store: CursorStore | None = None,
        types: str | None = None,
        batch_size: int = 100,
    ) -> None:
        self._client = client
        self._auth_token = auth_token
        self._poll_interval = poll_interval
        self._cursor_store: CursorStore = cursor_store or FileCursorStore()
        self._types = types
        self._batch_size = min(batch_size, 10000)
        self._handlers: dict[str, list[ChangeHandler]] = defaultdict(list)
        self._all_handlers: list[ChangeHandler] = []
        self._cursor: str | None = None
        self._cursor_loaded = False
        self._stop_event: asyncio.Event | None = None
        self._running = False

    def on(self, event_type: str, handler: ChangeHandler) -> None:
        """Register a handler for a specific event type.

        Supports glob patterns: "property.*" matches "property.created",
        "property.updated", etc.
        """
        self._handlers[event_type].append(handler)

    def on_all(self, handler: ChangeHandler) -> None:
        """Register a handler for all events."""
        self._all_handlers.append(handler)

    @property
    def cursor(self) -> str | None:
        """Current cursor position."""
        return self._cursor

    async def _load_cursor(self) -> None:
        """Load cursor from store on first use."""
        if not self._cursor_loaded:
            self._cursor = await self._cursor_store.load()
            self._cursor_loaded = True

    async def _dispatch(self, event: FeedEvent) -> None:
        """Dispatch a single event to matching handlers."""
        # Dispatch to type-specific handlers
        for pattern, handlers in self._handlers.items():
            if fnmatch(event.event_type, pattern):
                for handler in handlers:
                    try:
                        await handler(event)
                    except Exception:
                        logger.exception(
                            "Handler error for event %s (%s)",
                            event.event_id,
                            event.event_type,
                        )

        # Dispatch to catch-all handlers
        for handler in self._all_handlers:
            try:
                await handler(event)
            except Exception:
                logger.exception(
                    "Handler error for event %s (%s)",
                    event.event_id,
                    event.event_type,
                )

    async def poll_once(self) -> list[FeedEvent]:
        """Poll the feed once and dispatch events.

        Returns the list of events processed.
        """
        await self._load_cursor()

        try:
            page = await self._client.get_feed(
                auth_token=self._auth_token,
                cursor=self._cursor,
                types=self._types,
                limit=self._batch_size,
            )
        except RegistryError as e:
            if e.status_code == 410:
                logger.warning("Feed cursor expired, resetting to start")
                self._cursor = None
                await self._cursor_store.save("")
                return []
            raise

        for event in page.events:
            await self._dispatch(event)

        if page.cursor:
            self._cursor = page.cursor
            await self._cursor_store.save(page.cursor)

        return list(page.events)

    async def start(self) -> None:
        """Start the polling loop. Runs until stop() is called."""
        if self._running:
            return

        self._running = True
        self._stop_event = asyncio.Event()
        logger.info("RegistrySync started (interval=%.1fs)", self._poll_interval)

        try:
            while not self._stop_event.is_set():
                try:
                    events = await self.poll_once()
                    if events:
                        logger.debug("Processed %d events", len(events))
                except RegistryError as e:
                    logger.error("Feed poll failed: %s", e)
                except Exception:
                    logger.exception("Unexpected error in feed poll")

                # Wait for interval or stop signal
                try:
                    await asyncio.wait_for(
                        self._stop_event.wait(),
                        timeout=self._poll_interval,
                    )
                except asyncio.TimeoutError:
                    pass  # Normal - poll interval elapsed
        finally:
            self._running = False
            logger.info("RegistrySync stopped")

    async def stop(self) -> None:
        """Stop the polling loop gracefully."""
        if self._stop_event is not None:
            self._stop_event.set()

Polls the registry change feed and dispatches events to handlers.

Args
-----=
client
RegistryClient instance for HTTP calls.
auth_token
Bearer token for feed access.
poll_interval
Seconds between polls (default 60).
cursor_store
Optional CursorStore for persistence. Defaults to FileCursorStore.
types
Optional event type filter (e.g., "property.,agent.").
batch_size
Max events per poll (default 100, max 10000).

Instance variables

prop cursor : str | None
Expand source code
@property
def cursor(self) -> str | None:
    """Current cursor position."""
    return self._cursor

Current cursor position.

Methods

def on(self, event_type: str, handler: ChangeHandler) ‑> None
Expand source code
def on(self, event_type: str, handler: ChangeHandler) -> None:
    """Register a handler for a specific event type.

    Supports glob patterns: "property.*" matches "property.created",
    "property.updated", etc.
    """
    self._handlers[event_type].append(handler)

Register a handler for a specific event type.

Supports glob patterns: "property.*" matches "property.created", "property.updated", etc.

def on_all(self, handler: ChangeHandler) ‑> None
Expand source code
def on_all(self, handler: ChangeHandler) -> None:
    """Register a handler for all events."""
    self._all_handlers.append(handler)

Register a handler for all events.

async def poll_once(self) ‑> list[FeedEvent]
Expand source code
async def poll_once(self) -> list[FeedEvent]:
    """Poll the feed once and dispatch events.

    Returns the list of events processed.
    """
    await self._load_cursor()

    try:
        page = await self._client.get_feed(
            auth_token=self._auth_token,
            cursor=self._cursor,
            types=self._types,
            limit=self._batch_size,
        )
    except RegistryError as e:
        if e.status_code == 410:
            logger.warning("Feed cursor expired, resetting to start")
            self._cursor = None
            await self._cursor_store.save("")
            return []
        raise

    for event in page.events:
        await self._dispatch(event)

    if page.cursor:
        self._cursor = page.cursor
        await self._cursor_store.save(page.cursor)

    return list(page.events)

Poll the feed once and dispatch events.

Returns the list of events processed.

async def start(self) ‑> None
Expand source code
async def start(self) -> None:
    """Start the polling loop. Runs until stop() is called."""
    if self._running:
        return

    self._running = True
    self._stop_event = asyncio.Event()
    logger.info("RegistrySync started (interval=%.1fs)", self._poll_interval)

    try:
        while not self._stop_event.is_set():
            try:
                events = await self.poll_once()
                if events:
                    logger.debug("Processed %d events", len(events))
            except RegistryError as e:
                logger.error("Feed poll failed: %s", e)
            except Exception:
                logger.exception("Unexpected error in feed poll")

            # Wait for interval or stop signal
            try:
                await asyncio.wait_for(
                    self._stop_event.wait(),
                    timeout=self._poll_interval,
                )
            except asyncio.TimeoutError:
                pass  # Normal - poll interval elapsed
    finally:
        self._running = False
        logger.info("RegistrySync stopped")

Start the polling loop. Runs until stop() is called.

async def stop(self) ‑> None
Expand source code
async def stop(self) -> None:
    """Stop the polling loop gracefully."""
    if self._stop_event is not None:
        self._stop_event.set()

Stop the polling loop gracefully.

class ReportPlanAdjustmentRequest (**data: Any)
Expand source code
class ReportPlanAdjustmentRequest(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    action: Annotated[
        Action,
        Field(
            description='report is seller-authenticated and creates a non-authoritative record; review is plan-owner-authenticated and accepts or disputes it.'
        ),
    ]
    plan_id: Annotated[str, Field(description='Plan containing the source outcome.', min_length=1)]
    outcome_id: Annotated[
        str | None,
        Field(
            description='Completed outcome whose authoritative commitment is being adjusted.',
            min_length=1,
        ),
    ] = None
    adjustment_id: Annotated[
        str | None, Field(description='Adjustment to accept or dispute. Required for review.')
    ] = None
    decision: Annotated[
        Decision | None,
        Field(
            description='Buyer review decision. Acceptance is blocked while delivery evidence for the governed action is disputed in an open governance period; a historical closed_unresolved period is audit evidence, not a billing determination.'
        ),
    ] = None
    seller_reference: Annotated[
        str | None,
        Field(
            description='Seller resource identifier. Must exactly match the reference retained on the source outcome.',
            max_length=255,
            min_length=1,
        ),
    ] = None
    seller_adjustment_id: Annotated[
        str | None,
        Field(
            description="Stable identifier of the adjustment in the authenticated seller's system.",
            max_length=255,
            min_length=1,
        ),
    ] = None
    adjustment_type: Annotated[
        AdjustmentType | None,
        Field(
            description='Commercial meaning. Verified decommitments restore headroom in every mode; verified refunds and credits restore it only in verified_net_cost mode; makegoods never restore cash headroom.'
        ),
    ] = None
    amount: Annotated[
        Amount | None, Field(description='Positive adjustment amount in the plan currency.')
    ] = None
    reason: Annotated[
        str | None,
        Field(
            description='Human-readable reason retained in the audit trail.',
            max_length=1000,
            min_length=1,
        ),
    ] = None
    effective_at: Annotated[
        AwareDatetime | None, Field(description="When the seller's adjustment became effective.")
    ] = None
    evidence: Annotated[
        Evidence | None,
        Field(
            description='Integrity-bound commercial source record supplied by the seller. Its evidence_type must correspond to adjustment_type.'
        ),
    ] = None
    idempotency_key: Annotated[
        str,
        Field(
            description='Caller-generated retry key. Exact replays return the original report or review; reuse with another payload is rejected.',
            max_length=255,
            min_length=16,
            pattern='^[A-Za-z0-9_.:-]{16,255}$',
        ),
    ]
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var action : adcp.types.generated_poc.governance.report_plan_adjustment_request.Action
var adjustment_id : str | None
var adjustment_type : adcp.types.generated_poc.governance.report_plan_adjustment_request.AdjustmentType | None
var amount : adcp.types.generated_poc.governance.report_plan_adjustment_request.Amount | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var decision : adcp.types.generated_poc.governance.report_plan_adjustment_request.Decision | None
var effective_at : pydantic.types.AwareDatetime | None
var evidence : adcp.types.generated_poc.governance.report_plan_adjustment_request.Evidence | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var idempotency_key : str
var model_config
var outcome_id : str | None
var plan_id : str
var reason : str | None
var seller_adjustment_id : str | None
var seller_reference : str | None

Inherited members

class ReportPlanAdjustmentResponse (**data: Any)
Expand source code
class ReportPlanAdjustmentResponse(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    adjustment_id: Annotated[
        str, Field(description='Governance-agent identifier for the adjustment record.')
    ]
    adjustment_state: Annotated[
        AdjustmentState,
        Field(
            description='reported has seller evidence but no buyer decision; verified was accepted by the plan owner; disputed was rejected by the plan owner. Only verified records can affect net cost or headroom.'
        ),
    ]
    adjustment_type: AdjustmentType
    amount: Amount
    headroom_restored: Annotated[
        float,
        Field(
            description='Amount by which current ledger commitment was reduced under the plan accounting mode.',
            ge=0.0,
        ),
    ]
    plan_summary: PlanSummary
    replayed: Annotated[
        bool | None,
        Field(
            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."
        ),
    ] = False
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var adjustment_id : str
var adjustment_state : adcp.types.generated_poc.governance.report_plan_adjustment_response.AdjustmentState
var adjustment_type : adcp.types.generated_poc.governance.report_plan_adjustment_response.AdjustmentType
var amount : adcp.types.generated_poc.governance.report_plan_adjustment_response.Amount
var context : adcp.types.generated_poc.core.context.ContextObject | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var headroom_restored : float
var model_config
var plan_summary : adcp.types.generated_poc.governance.report_plan_adjustment_response.PlanSummary
var replayed : bool | None

Inherited members

class ReportPlanOutcomeRequest (**data: Any)
Expand source code
class ReportPlanOutcomeRequest(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    plan_id: Annotated[
        str,
        Field(
            description='The plan this outcome is for. The plan is owned by the authenticated buyer that synchronized it; plan_id is an identifier, not an account credential. Completed and failed settlements inherit their commercial binding from the exact approved check tuple.'
        ),
    ]
    check_id: Annotated[
        str | None,
        Field(
            description='The check_id from check_governance. Required for completed and failed outcomes and for buyer delivery observations. A delivery observation names the exact seller delivery check whose canonical statement is being compared.'
        ),
    ] = None
    idempotency_key: Annotated[
        str,
        Field(
            description='Buyer-generated unique key for this outcome report. An identical retry returns the cached response without another settlement; reuse with a different canonical payload returns IDEMPOTENCY_CONFLICT. Use a fresh UUID v4 for each distinct report.',
            max_length=255,
            min_length=16,
            pattern='^[A-Za-z0-9_.:-]{16,255}$',
        ),
    ]
    purchase_type: Annotated[
        purchase_type_1.PurchaseType | None,
        Field(
            description="The type of financial commitment this outcome is for. Must equal the original approved intent's purchase_type. Determines which budget allocation (if any) to charge against. Defaults to 'media_buy' when omitted."
        ),
    ] = purchase_type_1.PurchaseType.media_buy
    outcome: Annotated[outcome_type.OutcomeType, Field(description='Outcome type.')]
    seller_response: Annotated[
        SellerResponse | None,
        Field(description="The seller's full response. Required when outcome is 'completed'."),
    ] = None
    delivery: Annotated[
        Delivery | None,
        Field(
            description='Buyer-attributed observation compared with the canonical seller delivery statement identified by check_id. This evidence never overwrites seller evidence or creates a second commitment. A conflict produces an explicit disputed reconciliation state while the operational period is open; the plan owner may close it without asserting final billing truth.'
        ),
    ] = None
    error: Annotated[
        Error | None, Field(description="Error details. Required when outcome is 'failed'.")
    ] = None
    governance_context: Annotated[
        str | None,
        Field(
            description='Opaque governance context from the check_governance response. Required with check_id for completed and failed outcomes and buyer delivery observations.',
            max_length=4096,
            min_length=1,
            pattern='^[\\x20-\\x7E]+$',
        ),
    ] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var check_id : str | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var delivery : adcp.types.generated_poc.governance.report_plan_outcome_request.Delivery | None
var error : adcp.types.generated_poc.governance.report_plan_outcome_request.Error | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var governance_context : str | None
var idempotency_key : str
var model_config
var outcome : adcp.types.generated_poc.enums.outcome_type.OutcomeType
var plan_id : str
var purchase_type : adcp.types.generated_poc.enums.purchase_type.PurchaseType | None
var seller_response : adcp.types.generated_poc.governance.report_plan_outcome_request.SellerResponse | None

Inherited members

class ReportPlanOutcomeResponse (**data: Any)
Expand source code
class ReportPlanOutcomeResponse(AdcpVersionEnvelope):
    @model_validator(mode='before')
    @classmethod
    def _status_to_outcome_state(cls, data: Any) -> Any:
        if isinstance(data, dict) and 'outcome_state' not in data and 'status' in data:
            data = dict(data)
            data['outcome_state'] = data['status']
        return data

    model_config = ConfigDict(
        extra='allow',
    )
    outcome_id: Annotated[str, Field(description='Unique identifier for this outcome record.')]
    outcome_state: Annotated[
        OutcomeState,
        Field(
            description="Outcome state. 'accepted' means state updated with no issues. 'findings' means issues were detected. Renamed from `status` in 3.1 to free the top-level `status` key for the envelope task-status (TaskStatus) under MCP flat-on-the-wire serialization."
        ),
    ]
    committed_budget: Annotated[
        float | None,
        Field(
            description="Budget committed from this outcome. Present for 'completed' and 'failed' outcomes."
        ),
    ] = None
    delivery_reconciliation_status: Annotated[
        DeliveryReconciliationStatus | None,
        Field(
            description='Comparison state between the buyer-attributed observation and canonical seller statement. Present for delivery outcomes. A disputed state blocks adjustment verification while the period is open. measurement_variance records a buyer-measured cumulative_spend that differs from the seller statement; it never blocks verification — the higher amount bounds conservative exposure and decommitments instead. closed_unresolved preserves the discrepancy after operational closure without claiming a final billing result.'
        ),
    ] = None
    delivery_period_state: Annotated[
        DeliveryPeriodState | None,
        Field(
            description='Operational state of the governance reporting period. Closure freezes governance evidence for that period but is not billing settlement.'
        ),
    ] = None
    findings: Annotated[
        list[Finding] | None,
        Field(description="Issues detected. Present only when outcome_state is 'findings'."),
    ] = None
    plan_summary: Annotated[
        PlanSummary | None,
        Field(
            description="Updated plan budget state. Present for 'completed' and 'failed' outcomes."
        ),
    ] = None
    replayed: Annotated[
        bool | None,
        Field(
            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."
        ),
    ] = False
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var committed_budget : float | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var delivery_period_state : adcp.types.generated_poc.governance.report_plan_outcome_response.DeliveryPeriodState | None
var delivery_reconciliation_status : adcp.types.generated_poc.governance.report_plan_outcome_response.DeliveryReconciliationStatus | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var findings : list[adcp.types.generated_poc.governance.report_plan_outcome_response.Finding] | None
var model_config
var outcome_id : str
var outcome_state : adcp.types.generated_poc.governance.report_plan_outcome_response.OutcomeState
var plan_summary : adcp.types.generated_poc.governance.report_plan_outcome_response.PlanSummary | None
var replayed : bool | None

Inherited members

class ReportUsageRequest (**data: Any)
Expand source code
class ReportUsageRequest(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    idempotency_key: Annotated[
        str,
        Field(
            description='Client-generated unique key for this request. If a request with the same key has already been accepted, the server returns the original response without re-processing. MUST be unique per (seller, request) pair to prevent cross-seller correlation. Use a fresh UUID v4 for each request. Prevents duplicate billing on retries.'
        ),
    ]
    reporting_period: Annotated[
        datetime_range.DatetimeRange,
        Field(
            description='The time range covered by this usage report. Applies to all records in the request.'
        ),
    ]
    usage: Annotated[
        list[UsageItem],
        Field(
            description='One or more usage records. Each record is self-contained: it carries its own account, allowing a single request to span multiple accounts.',
            min_length=1,
        ),
    ]
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var idempotency_key : str
var model_config
var reporting_period : adcp.types.generated_poc.core.datetime_range.DatetimeRange
var usage : list[adcp.types.generated_poc.account.report_usage_request.UsageItem]

Inherited members

class ReportUsageResponse (**data: Any)
Expand source code
class ReportUsageResponse(AdcpVersionEnvelope, ProtocolEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    accepted: Annotated[
        int, Field(description='Number of usage records successfully stored.', ge=0)
    ]
    errors: Annotated[
        list[error.Error] | None,
        Field(
            description="Validation errors for individual records. The field property identifies which record failed (e.g., 'usage[1].pricing_option_id')."
        ),
    ] = None
    sandbox: Annotated[
        bool | None,
        Field(description='When true, the account is a sandbox account and no billing occurred.'),
    ] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • adcp.types.generated_poc.core.protocol_envelope.ProtocolEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var accepted : int
var context : adcp.types.generated_poc.core.context.ContextObject | None
var errors : list[adcp.types.generated_poc.core.error.Error] | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config
var sandbox : bool | None

Inherited members

class RequestProposalsRequest (**data: Any)
Expand source code
class RequestProposalsRequest(AdCPBaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    adcp_version: version_envelope.AdcpVersion | None = None
    adcp_major_version: version_envelope.AdcpMajorVersion | None = None
    context_id: Annotated[str | None, Field(min_length=1)] = None
    context: context_1.ContextObject | None = None
    governance_context: Annotated[str | None, Field(max_length=4096, min_length=1)] = None
    push_notification_config: push_notification_config_1.PushNotificationConfig | None = None
    idempotency_key: Annotated[
        str,
        Field(
            description='Client-generated key required for retry-safe proposal creation.',
            max_length=255,
            min_length=16,
            pattern='^[A-Za-z0-9_.:-]{16,255}$',
        ),
    ]
    account: Annotated[
        canonical_account_ref.CanonicalAccountReference | None,
        Field(
            description='Alternative brand source for proposal terms. Provide either this natural-key account containing brand and operator or top-level brand, not both.'
        ),
    ] = None
    brand: Annotated[
        brand_key.BrandKey | None,
        Field(
            description='Alternative brand source for proposal terms. Provide either top-level brand or a natural-key account containing brand and operator, not both.'
        ),
    ] = None
    brief: Annotated[
        str,
        Field(
            description='Campaign goal, strategy, and requirements that are not represented in structured criteria.',
            min_length=1,
        ),
    ]
    criteria: product_discovery_criteria.ProductDiscoveryCriteria | None = None
    opportunity: Annotated[
        Opportunity | None,
        Field(
            description='Optional planning-cycle context that the seller associates with every proposal created by this request.'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var account : adcp.types.generated_poc.core.canonical_account_ref.CanonicalAccountReference | None
var adcp_major_version : adcp.types.generated_poc.core.version_envelope.AdcpMajorVersion | None
var adcp_version : adcp.types.generated_poc.core.version_envelope.AdcpVersion | None
var brand : adcp.types.generated_poc.core.brand_key.BrandKey | None
var brief : str
var context : adcp.types.generated_poc.core.context.ContextObject | None
var context_id : str | None
var criteria : adcp.types.generated_poc.media_buy.product_discovery_criteria.ProductDiscoveryCriteria | None
var governance_context : str | None
var idempotency_key : str
var model_config
var opportunity : adcp.types.generated_poc.media_buy.request_proposals_request.Opportunity | None
var push_notification_config : adcp.types.generated_poc.core.push_notification_config.PushNotificationConfig | None

Inherited members

class ResolvedBrand (**data: Any)
Expand source code
class ResolvedBrand(BaseModel):
    """Brand identity resolved from the AdCP registry."""

    model_config = ConfigDict(extra="allow")

    canonical_id: str
    canonical_domain: str
    brand_name: str
    names: list[dict[str, str]] | None = None
    keller_type: str | None = None
    parent_brand: str | None = None
    house_domain: str | None = None
    house_name: str | None = None
    brand_agent_url: str | None = None
    brand: dict[str, Any] | None = None
    source: str

Brand identity resolved from the AdCP registry.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • pydantic.main.BaseModel

Class variables

var brand : dict[str, typing.Any] | None
var brand_agent_url : str | None
var brand_name : str
var canonical_domain : str
var canonical_id : str
var house_domain : str | None
var house_name : str | None
var keller_type : str | None
var model_config
var names : list[dict[str, str]] | None
var parent_brand : str | None
var source : str
class ResolvedProperty (**data: Any)
Expand source code
class ResolvedProperty(BaseModel):
    """Property information resolved from the AdCP registry."""

    model_config = ConfigDict(extra="allow")

    publisher_domain: str
    source: str
    authorized_agents: list[dict[str, Any]]
    properties: list[dict[str, Any]]
    verified: bool

Property information resolved from the AdCP registry.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • pydantic.main.BaseModel

Class variables

var authorized_agents : list[dict[str, typing.Any]]
var model_config
var properties : list[dict[str, typing.Any]]
var publisher_domain : str
var source : str
var verified : bool
class ResponsePayloadJwsEnvelope (**data: Any)
Expand source code
class ResponsePayloadJwsEnvelope(AdCPBaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    protected: Annotated[
        str,
        Field(
            description='Base64url-encoded JWS protected header. The decoded header MUST include alg, kid, and typ: adcp-response-payload+jws, and MUST NOT include the RFC 7797 b64 header. Verifiers enforce the key purpose by resolving kid to a JWK with adcp_use: response-signing.',
            pattern='^[A-Za-z0-9_-]+$',
        ),
    ]
    payload: Annotated[
        ResponsePayload,
        Field(
            description='Decoded signed payload. Signers compute the JWS payload bytes from the RFC 8785/JCS canonicalization of this object.'
        ),
    ]
    signature: Annotated[
        str,
        Field(
            description='Base64url-encoded JWS signature over the protected header and canonicalized payload.',
            pattern='^[A-Za-z0-9_-]+$',
        ),
    ]

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var model_config
var payload : adcp.types.generated_poc.core.response_payload_jws_envelope.ResponsePayload
var protected : str
var signature : str

Inherited members

class SchemaValidationError (tool: str,
side: str,
issues: list[ValidationIssue],
message: str | None = None)
Expand source code
class SchemaValidationError(Exception):
    """Raised by strict-mode client hooks when a payload fails schema.

    Carries the full issue list via :attr:`issues` so callers can inspect
    every JSON Pointer, not just the first. Mirrors the shape of the AdCP
    L3 ``VALIDATION_ERROR`` error envelope.

    Attributes:
        tool: AdCP tool name that was being validated.
        side: ``"request"`` or ``"response"``.
        issues: Every failure, each with a sanitized message.
        code: Always ``"VALIDATION_ERROR"``.
        details: Structured payload mirroring the wire error envelope's
            ``details`` shape — tool/side/issues, ready for programmatic
            inspection by callers that don't want to parse the exception
            message.
    """

    tool: str
    side: str
    issues: list[ValidationIssue]
    code: str
    details: dict[str, Any]

    def __init__(
        self,
        tool: str,
        side: str,
        issues: list[ValidationIssue],
        message: str | None = None,
    ) -> None:
        self.tool = tool
        self.side = side
        self.issues = issues
        self.code = "VALIDATION_ERROR"
        self.details = {
            "tool": tool,
            "side": side,
            "issues": [_issue_to_wire(i) for i in issues],
        }
        if message is None:
            first = issues[0] if issues else None
            if first is not None:
                message = (
                    f"{tool} {side} failed schema validation at "
                    f"{first.pointer}: {first.message}"
                )
            else:
                message = f"{tool} {side} failed schema validation"
        super().__init__(message)

Raised by strict-mode client hooks when a payload fails schema.

Carries the full issue list via :attr:issues so callers can inspect every JSON Pointer, not just the first. Mirrors the shape of the AdCP L3 VALIDATION_ERROR error envelope.

Attributes
-----=
tool
AdCP tool name that was being validated.
side
"request" or "response".
issues
Every failure, each with a sanitized message.
code
Always "VALIDATION_ERROR".
details
Structured payload mirroring the wire error envelope's details shape — tool/side/issues, ready for programmatic inspection by callers that don't want to parse the exception message.

Ancestors

  • builtins.Exception
  • builtins.BaseException

Class variables

var code : str
var details : dict[str, typing.Any]
var issues : list[ValidationIssue]
var side : str
var tool : str
class SellerAgentReference (**data: Any)
Expand source code
class SellerAgentReference(AdCPBaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    agent_url: Annotated[
        AnyUrl,
        Field(
            description="The seller agent's API endpoint URL as declared in the property publisher's adagents.json `authorized_agents[].url`. MUST use the `https://` scheme. Receivers compare this URL against the `authorized_agents` list using the AdCP URL canonicalization rules — not byte-equality — and reject mismatches with `seller_not_authorized`. See docs/reference/url-canonicalization."
        ),
    ]
    id: Annotated[
        str | None,
        Field(
            description='Reserved for a future registry-assigned stable seller identifier. Not used today — senders MUST NOT populate this field until a registry is defined. When a future release populates both `agent_url` and `id`, `agent_url` remains authoritative and `id` is advisory.',
            min_length=1,
            pattern='^[a-zA-Z0-9_-]+$',
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var agent_url : pydantic.networks.AnyUrl
var id : str | None
var model_config

Inherited members

class SiSendActionResponseRequest (**data: Any)
Expand source code
class SiSendMessageRequest(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    idempotency_key: Annotated[
        str,
        Field(
            description='Client-generated unique key for at-most-once execution. Each conversational turn is a distinct mutation of session transcript — without this key, a timeout-and-retry produces a duplicate turn and a duplicate model response. MUST be unique per (seller, request) pair. Use a fresh UUID v4 for each user turn.',
            max_length=255,
            min_length=16,
            pattern='^[A-Za-z0-9_.:-]{16,255}$',
        ),
    ]
    session_id: Annotated[str, Field(description='Active session identifier')]
    message: Annotated[str | None, Field(description="User's message to the brand agent")] = None
    action_response: Annotated[
        ActionResponse | None,
        Field(description='Response to a previous action_button (e.g., user clicked checkout)'),
    ] = None
    sponsored_context_receipt: Annotated[
        si_sponsored_context_receipt.SiSponsoredContextReceipt | None,
        Field(
            description="Host receipt for sponsored context accepted from a prior SI response in this session. This gives the brand/seller an audit-visible record of the host's accepted use mode and disclosure commitment for that context."
        ),
    ] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var action_response : adcp.types.generated_poc.sponsored_intelligence.si_send_message_request.ActionResponse | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var idempotency_key : str
var message : str | None
var model_config
var session_id : str
var sponsored_context_receipt : adcp.types.generated_poc.sponsored_intelligence.si_sponsored_context_receipt.SiSponsoredContextReceipt | None
class SiSendTextMessageRequest (**data: Any)
Expand source code
class SiSendMessageRequest(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    idempotency_key: Annotated[
        str,
        Field(
            description='Client-generated unique key for at-most-once execution. Each conversational turn is a distinct mutation of session transcript — without this key, a timeout-and-retry produces a duplicate turn and a duplicate model response. MUST be unique per (seller, request) pair. Use a fresh UUID v4 for each user turn.',
            max_length=255,
            min_length=16,
            pattern='^[A-Za-z0-9_.:-]{16,255}$',
        ),
    ]
    session_id: Annotated[str, Field(description='Active session identifier')]
    message: Annotated[str | None, Field(description="User's message to the brand agent")] = None
    action_response: Annotated[
        ActionResponse | None,
        Field(description='Response to a previous action_button (e.g., user clicked checkout)'),
    ] = None
    sponsored_context_receipt: Annotated[
        si_sponsored_context_receipt.SiSponsoredContextReceipt | None,
        Field(
            description="Host receipt for sponsored context accepted from a prior SI response in this session. This gives the brand/seller an audit-visible record of the host's accepted use mode and disclosure commitment for that context."
        ),
    ] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var action_response : adcp.types.generated_poc.sponsored_intelligence.si_send_message_request.ActionResponse | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var idempotency_key : str
var message : str | None
var model_config
var session_id : str
var sponsored_context_receipt : adcp.types.generated_poc.sponsored_intelligence.si_sponsored_context_receipt.SiSponsoredContextReceipt | None

Inherited members

class SignalAvailabilityType (*args, **kwds)
Expand source code
class SignalAvailabilityType(StrEnum):
    marketplace = 'marketplace'
    custom = 'custom'
    owned = 'owned'

Enum where members are also (and must be) strings

Ancestors

  • enum.StrEnum
  • builtins.str
  • enum.ReprEnum
  • enum.Enum

Class variables

var custom
var marketplace
var owned
class SignalCatalogType (*args, **kwds)
Expand source code
class SignalAvailabilityType(StrEnum):
    marketplace = 'marketplace'
    custom = 'custom'
    owned = 'owned'

Enum where members are also (and must be) strings

Ancestors

  • enum.StrEnum
  • builtins.str
  • enum.ReprEnum
  • enum.Enum

Class variables

var custom
var marketplace
var owned
class SignalDefinitionEnrichment (**data: Any)
Expand source code
class SignalDefinitionEnrichment(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    restricted_attributes: Annotated[
        list[restricted_attribute.RestrictedAttribute] | None,
        Field(description='Restricted attribute categories this signal touches.', min_length=1),
    ] = None
    demographic_predicate: Annotated[
        demographic_predicate_1.DemographicPredicate | None,
        Field(
            description="Projected authoritative demographic meaning for the signal. When projected from another provider, this MUST match the provider's definition exactly. Signal names alone never establish demographic semantics."
        ),
    ] = None
    policy_categories: Annotated[
        list[str] | None,
        Field(description='Policy categories this signal is sensitive for.', min_length=1),
    ] = None
    taxonomy: Annotated[
        Taxonomy | None,
        Field(
            description='Optional taxonomy metadata describing what this signal means in an external audience, content, retail-media, or provider-owned taxonomy.'
        ),
    ] = None
    segmentation_criteria: Annotated[str | None, Field(max_length=500)] = None
    criteria_url: AnyUrl | None = None
    data_sources: Annotated[list[DataSource] | None, Field(min_length=1)] = None
    methodology: Methodology | None = None
    audience_expansion: bool | None = None
    device_expansion: bool | None = None
    refresh_cadence: RefreshCadence | None = None
    lookback_window: RefreshCadence | None = None
    onboarder: Onboarder | None = None
    countries: Annotated[list[Country] | None, Field(min_length=1)] = None
    consent_basis: Annotated[
        list[consent_basis_1.ConsentBasis] | None,
        Field(
            description="Data provider's declared GDPR Article 6 lawful basis or consent basis for the underlying signal definition, projected into this get_signals response row when requested. Sellers and federating agents that pass through another provider's signal MUST NOT substitute their own processing basis for the provider-declared basis.",
            min_length=1,
        ),
    ] = None
    art9_basis: Annotated[
        Art9Basis | None,
        Field(
            description="Data provider's declared GDPR Article 9 basis for the underlying signal definition when special-category data is involved and Article 9 applies, projected into this get_signals response row when requested. Sellers and federating agents that pass through another provider's signal MUST NOT substitute their own Article 9 basis for the provider-declared basis."
        ),
    ] = None
    modeling: Modeling | None = None
    data_subject_rights: Annotated[
        DataSubjectRights | None,
        Field(
            description='Per-signal data-subject-rights routing. This is a contact/routing reference, not a machine-callable AdCP API.'
        ),
    ] = None
    last_updated: Annotated[
        AwareDatetime | None,
        Field(
            description='When this definition record was last updated. This indicates freshness of the definition record, not an attestation that the underlying data or model was refreshed at that time.'
        ),
    ] = None
    dts_compliant_version: str | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var art9_basis : adcp.types.generated_poc.core.signal_definition_enrichment.Art9Basis | None
var audience_expansion : bool | None
var consent_basis : list[adcp.types.generated_poc.enums.consent_basis.ConsentBasis] | None
var countries : list[adcp.types.generated_poc.core.signal_definition_enrichment.Country] | None
var criteria_url : pydantic.networks.AnyUrl | None
var data_sources : list[adcp.types.generated_poc.core.signal_definition_enrichment.DataSource] | None
var data_subject_rights : adcp.types.generated_poc.core.signal_definition_enrichment.DataSubjectRights | None
var demographic_predicate : adcp.types.generated_poc.core.demographic_predicate.DemographicPredicate | None
var device_expansion : bool | None
var dts_compliant_version : str | None
var last_updated : pydantic.types.AwareDatetime | None
var lookback_window : adcp.types.generated_poc.core.signal_definition_enrichment.RefreshCadence | None
var methodology : adcp.types.generated_poc.core.signal_definition_enrichment.Methodology | None
var model_config
var modeling : adcp.types.generated_poc.core.signal_definition_enrichment.Modeling | None
var onboarder : adcp.types.generated_poc.core.signal_definition_enrichment.Onboarder | None
var policy_categories : list[str] | None
var refresh_cadence : adcp.types.generated_poc.core.signal_definition_enrichment.RefreshCadence | None
var restricted_attributes : list[adcp.types.generated_poc.enums.restricted_attribute.RestrictedAttribute] | None
var segmentation_criteria : str | None
var taxonomy : adcp.types.generated_poc.core.signal_definition_enrichment.Taxonomy | None

Inherited members

class SignalFilters (**data: Any)
Expand source code
class SignalFilters(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    catalog_types: Annotated[
        list[signal_catalog_type.SignalAvailabilityType] | None,
        Field(description='Filter by catalog type', min_length=1),
    ] = None
    data_providers: Annotated[
        list[str] | None, Field(description='Filter by specific data providers', min_length=1)
    ] = None
    max_cpm: Annotated[
        float | None,
        Field(description="Maximum CPM filter. Applies only to signals with model='cpm'.", ge=0.0),
    ] = None
    max_percent: Annotated[
        float | None,
        Field(
            description='Maximum percent-of-media rate filter. Signals where all percent_of_media pricing options exceed this value are excluded. Does not account for max_cpm caps.',
            ge=0.0,
            le=100.0,
        ),
    ] = None
    min_coverage_percentage: Annotated[
        float | None, Field(description='Minimum coverage requirement', ge=0.0, le=100.0)
    ] = None
    ext: Annotated[
        ext_1.ExtensionObject | None,
        Field(
            description='Vendor-namespaced extension parameters for seller- or platform-specific signal filter criteria not covered by standard fields. Keys MUST be namespaced under a vendor or platform key (e.g., ext.gam, ext.platform_x). Sellers MUST treat all values as untrusted buyer input; avoid unbounded logging or labels, and do not interpolate values into caller-visible error strings, LLM prompts, SQL queries, or system commands without sanitization. Persistent use of an extension key across multiple buyers is a signal to propose standardization.'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var catalog_types : list[adcp.types.generated_poc.enums.signal_catalog_type.SignalAvailabilityType] | None
var data_providers : list[str] | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var max_cpm : float | None
var max_percent : float | None
var min_coverage_percentage : float | None
var model_config

Inherited members

class SignalListing (**data: Any)
Expand source code
class SignalListing(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    signal_ref: Annotated[
        signal_ref_1.SignalRef | None,
        Field(
            description="Canonical signal reference. Use scope 'product' for a product-local signal defined by this listing; use scope 'data_provider' with data_provider_domain for a signal defined in a data provider's published adagents.json signals[]; use scope 'signal_source' with signal_source_url for a source-native signal."
        ),
    ] = None
    signal_id: Annotated[
        signal_id_1.SignalId | None,
        Field(
            deprecated=True,
            description='DEPRECATED. Use signal_ref instead. Legacy SignalId retained for compatibility with older Signals Protocol clients.',
        ),
    ] = None
    name: Annotated[
        str | None,
        Field(
            description="Human-readable signal name. Required when signal_ref.scope is 'product'. For data_provider and signal_source refs, this is optional contextual display text; the referenced definition or source remains authoritative."
        ),
    ] = None
    description: Annotated[
        str | None,
        Field(
            description='Detailed signal description. For data_provider and signal_source refs, this is optional contextual display text and MUST NOT replace the referenced definition.'
        ),
    ] = None
    methodology_url: Annotated[
        AnyUrl | None,
        Field(
            description='Optional link to published methodology, media-kit, or data documentation. For data_provider and signal_source refs, this SHOULD match or supplement the referenced definition.'
        ),
    ] = None
    last_updated: Annotated[
        AwareDatetime | None,
        Field(
            description='When this listing record was last updated. This indicates freshness of the listing record, not an attestation that the underlying data or model was refreshed at that time.'
        ),
    ] = None
    value_type: Annotated[
        signal_value_type.SignalValueType | None,
        Field(
            description="The data type of this signal's values. Required when signal_ref.scope is 'product'."
        ),
    ] = None
    categories: Annotated[
        list[str] | None,
        Field(
            description="Valid values for categorical signals. Present when value_type is 'categorical'.",
            min_length=1,
        ),
    ] = None
    range: Annotated[
        Range | None,
        Field(description="Valid range for numeric signals. Present when value_type is 'numeric'."),
    ] = None
    restricted_attributes: Annotated[
        list[restricted_attribute.RestrictedAttribute] | None,
        Field(
            description='Restricted attribute categories this listing touches. Required with demographic_predicate and must include age. For referenced provider/source signals, any projected values must match the authoritative definition.',
            min_length=1,
        ),
    ] = None
    demographic_predicate: Annotated[
        demographic_predicate_1.DemographicPredicate | None,
        Field(
            description='Machine-readable demographic meaning. For product-local signals this listing is authoritative; for data-provider and signal-source refs, any projected value MUST match the referenced authoritative definition. Signal names alone never establish demographic semantics.'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Subclasses

  • adcp.types.generated_poc.core.product_signal_targeting_option.ProductSignalTargetingOption
  • adcp.types.generated_poc.core.wholesale_feed_event.Signal

Class variables

var categories : list[str] | None
var demographic_predicate : adcp.types.generated_poc.core.demographic_predicate.DemographicPredicate | None
var description : str | None
var last_updated : pydantic.types.AwareDatetime | None
var methodology_url : pydantic.networks.AnyUrl | None
var model_config
var name : str | None
var range : adcp.types.generated_poc.core.signal_listing.Range | None
var restricted_attributes : list[adcp.types.generated_poc.enums.restricted_attribute.RestrictedAttribute] | None
var signal_id : adcp.types.generated_poc.core.signal_id.SignalId | None
var signal_ref : adcp.types.generated_poc.core.signal_ref.SignalRef | None
var value_type : adcp.types.generated_poc.enums.signal_value_type.SignalValueType | None

Inherited members

class SignalPricingOption (root: RootModelRootType = PydanticUndefined, **data)
Expand source code
class SignalPricingOption(RootModel[vendor_pricing_option.VendorPricingOption]):
    root: Annotated[
        vendor_pricing_option.VendorPricingOption,
        Field(
            description='Deprecated — use vendor-pricing-option.json for new implementations. This alias is retained for backward compatibility.',
            title='Signal Pricing Option',
        ),
    ]

Usage Documentation

RootModel and Custom Root Types

A Pydantic BaseModel for the root object of the model.

Attributes
-----=
root
The root object of the model.
__pydantic_root_model__
Whether the model is a RootModel.
__pydantic_private__
Private fields in the model.
__pydantic_extra__
Extra fields in the model.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • pydantic.root_model.RootModel[VendorPricingOption]
  • pydantic.root_model.RootModel
  • pydantic.main.BaseModel
  • typing.Generic

Class variables

var model_config
var root : adcp.types.generated_poc.core.vendor_pricing_option.VendorPricingOption
class SignalRef (root: RootModelRootType = PydanticUndefined, **data)
Expand source code
class SignalRef(RootModel[SignalRef1 | SignalRef2 | SignalRef3]):
    root: Annotated[
        SignalRef1 | SignalRef2 | SignalRef3,
        Field(
            description="Reference to a named signal definition. Uses scope as discriminator: 'data_provider' for a signal resolved through published adagents.json signals[], 'signal_source' for a source-native signal resolved through the issuing signal source, or 'product' for a product-local signal option. Scope is the resolution path, not provenance; authoritative enrichment lives on the seller, signal source, or data-provider signal definition, not on this reference.",
            discriminator='scope',
            title='Signal Ref',
        ),
    ]
    def __getattr__(self, name: str) -> Any:
        """Proxy attribute access to the wrapped type."""
        if name.startswith('_'):
            raise AttributeError(name)
        return getattr(self.root, name)

Usage Documentation

RootModel and Custom Root Types

A Pydantic BaseModel for the root object of the model.

Attributes
-----=
root
The root object of the model.
__pydantic_root_model__
Whether the model is a RootModel.
__pydantic_private__
Private fields in the model.
__pydantic_extra__
Extra fields in the model.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • pydantic.root_model.RootModel[Union[SignalRef1, SignalRef2, SignalRef3]]
  • pydantic.root_model.RootModel
  • pydantic.main.BaseModel
  • typing.Generic

Class variables

var model_config
var root : adcp.types.generated_poc.core.signal_ref.SignalRef1 | adcp.types.generated_poc.core.signal_ref.SignalRef2 | adcp.types.generated_poc.core.signal_ref.SignalRef3
class SignalTargeting (root: RootModelRootType = PydanticUndefined, **data)
Expand source code
class SignalTargeting(RootModel[SignalTargeting1 | SignalTargeting2 | SignalTargeting3]):
    root: Annotated[
        SignalTargeting1 | SignalTargeting2 | SignalTargeting3,
        Field(
            description='Targeting constraint for a specific signal. Uses value_type as discriminator to determine the targeting expression format.',
            discriminator='value_type',
            title='Signal Targeting',
        ),
    ]
    def __getattr__(self, name: str) -> Any:
        """Proxy attribute access to the wrapped type."""
        if name.startswith('_'):
            raise AttributeError(name)
        return getattr(self.root, name)

Usage Documentation

RootModel and Custom Root Types

A Pydantic BaseModel for the root object of the model.

Attributes
-----=
root
The root object of the model.
__pydantic_root_model__
Whether the model is a RootModel.
__pydantic_private__
Private fields in the model.
__pydantic_extra__
Extra fields in the model.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • pydantic.root_model.RootModel[Union[SignalTargeting1, SignalTargeting2, SignalTargeting3]]
  • pydantic.root_model.RootModel
  • pydantic.main.BaseModel
  • typing.Generic

Class variables

var model_config
var root : adcp.types.generated_poc.core.signal_targeting.SignalTargeting1 | adcp.types.generated_poc.core.signal_targeting.SignalTargeting2 | adcp.types.generated_poc.core.signal_targeting.SignalTargeting3
class SignalTargetingExpression (root: RootModelRootType = PydanticUndefined, **data)
Expand source code
class SignalTargetingExpression(
    RootModel[SignalTargetingExpression1 | SignalTargetingExpression2 | SignalTargetingExpression3]
):
    root: Annotated[
        SignalTargetingExpression1 | SignalTargetingExpression2 | SignalTargetingExpression3,
        Field(
            description='Predicate over a named signal definition. Signals are typed dimensions, similar to feature values: binary signals match true, categorical signals match one of a set of values, and numeric signals match a range. In package signal targeting groups, include/exclude semantics are controlled by the parent group operator, not by negating the expression.',
            discriminator='value_type',
            title='Signal Targeting Expression',
        ),
    ]
    def __getattr__(self, name: str) -> Any:
        """Proxy attribute access to the wrapped type."""
        if name.startswith('_'):
            raise AttributeError(name)
        return getattr(self.root, name)

Usage Documentation

RootModel and Custom Root Types

A Pydantic BaseModel for the root object of the model.

Attributes
-----=
root
The root object of the model.
__pydantic_root_model__
Whether the model is a RootModel.
__pydantic_private__
Private fields in the model.
__pydantic_extra__
Extra fields in the model.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • pydantic.root_model.RootModel[Union[SignalTargetingExpression1, SignalTargetingExpression2, SignalTargetingExpression3]]
  • pydantic.root_model.RootModel
  • pydantic.main.BaseModel
  • typing.Generic

Class variables

var model_config
var root : adcp.types.generated_poc.core.signal_targeting_expression.SignalTargetingExpression1 | adcp.types.generated_poc.core.signal_targeting_expression.SignalTargetingExpression2 | adcp.types.generated_poc.core.signal_targeting_expression.SignalTargetingExpression3
class SignalTargetingRules (**data: Any)
Expand source code
class SignalTargetingRules(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    resolution_model: Annotated[
        ResolutionModel | None,
        Field(
            description="How selected signal_targeting_options are resolved against the product's inventory. 'direct_targeting' means selected signals are applied as targeting predicates to the package inventory. 'seller_planned' means selected signals are planning inputs that the seller resolves against product-specific inventory, timing, availability, reach, or pacing constraints; buyers SHOULD NOT attempt to decompose the signal selection into lower-level inventory or schedule decisions. Use 'seller_planned' for products such as linear broadcast schedules where the audience definition may be portable but the audience-to-avails plan is seller-resolved."
        ),
    ] = ResolutionModel.direct_targeting
    selection_mode: Annotated[
        SelectionMode | None,
        Field(
            description="Default selection behavior for selectable signals on this product. 'optional' means the buyer may select zero or more signals. 'required' means the buyer must select at least min_selected_signals, or 1 when min_selected_signals is omitted. 'fixed' means the seller applies the default_selected signals and the buyer cannot add or remove them; buyers SHOULD render those entries as read-only and sellers MUST echo them in package targeting_overlay.signal_targeting_groups. Use selection_group_rules for product-scoped products that need different behavior for different groups, such as fixed suppressions plus a required include tier."
        ),
    ] = SelectionMode.optional
    min_selected_signals: Annotated[
        int | None,
        Field(
            description="Minimum number of signals the buyer must select when selection_mode is 'required'. If selection_mode is 'required' and this field is omitted, sellers MUST treat the minimum as 1. Defaults to 0 for optional selection.",
            ge=0,
        ),
    ] = None
    max_selected_signals: Annotated[
        int | None,
        Field(
            description='Maximum number of signals the buyer may select for a package. Omit when there is no declared limit beyond the available options.',
            ge=1,
        ),
    ] = None
    max_selected_per_group: Annotated[
        int | None,
        Field(
            description='Maximum number of signal_targeting_options the buyer may select from the same ProductSignalTargetingOption.selection_group. Use 1 for mutually exclusive alternatives within each option group. This limit applies to product option grouping, not to the number of child groups in packages[].targeting_overlay.signal_targeting_groups.',
            ge=1,
        ),
    ] = None
    max_signal_targeting_groups: Annotated[
        int | None,
        Field(
            description='Maximum number of child groups allowed in packages[].targeting_overlay.signal_targeting_groups.groups. Omit when the seller has no declared limit beyond product terms.',
            ge=1,
        ),
    ] = None
    max_signals_per_targeting_group: Annotated[
        int | None,
        Field(
            description='Maximum number of signals allowed in each packages[].targeting_overlay.signal_targeting_groups.groups[].signals array. Omit when the seller has no declared limit beyond product terms.',
            ge=1,
        ),
    ] = None
    selection_group_rules: Annotated[
        list[signal_selection_group_rule.SignalSelectionGroupRule] | None,
        Field(
            description='Optional product-scoped overrides for specific ProductSignalTargetingOption.selection_group values. Use this when one product has mixed behavior, such as fixed seller-applied suppressions, a required pick-one include tier, optional buyer-selected exclusions, or heterogeneous targeting planes that must be represented as separate ANDed clauses. Rules apply only to options whose selection_group matches. When selection_group_rules are present, each packages[].targeting_overlay.signal_targeting_groups child group MUST contain signals from exactly one selection_group and one targeting_mode, and buyers MUST send at most one child group for each (selection_group, targeting_mode) pair. Sellers MUST reject duplicate, mixed, or collapsed groups that combine distinct selection_group_rules into the same child group.',
            min_length=1,
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var max_selected_per_group : int | None
var max_selected_signals : int | None
var max_signal_targeting_groups : int | None
var max_signals_per_targeting_group : int | None
var min_selected_signals : int | None
var model_config
var resolution_model : adcp.types.generated_poc.core.signal_targeting_rules.ResolutionModel | None
var selection_group_rules : list[adcp.types.generated_poc.core.signal_selection_group_rule.SignalSelectionGroupRule] | None
var selection_mode : adcp.types.generated_poc.core.signal_targeting_rules.SelectionMode | None

Inherited members

class Snapshot (**data: Any)
Expand source code
class Snapshot(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    as_of: Annotated[
        AwareDatetime, Field(description='When this snapshot was captured by the platform')
    ]
    staleness_seconds: Annotated[
        int,
        Field(
            description='Maximum age of this data in seconds. For example, 3600 means the data may be up to 1 hour old.',
            ge=0,
        ),
    ]
    impressions: Annotated[
        int,
        Field(
            description='Lifetime impressions across all assignments. Not scoped to any date range.',
            ge=0,
        ),
    ]
    last_served: Annotated[
        AwareDatetime | None,
        Field(
            description='Last time this creative served an impression. Absent when the creative has never served.'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var as_of : pydantic.types.AwareDatetime
var impressions : int
var last_served : pydantic.types.AwareDatetime | None
var model_config
var staleness_seconds : int

Inherited members

class SnapshotUnavailableReason (*args, **kwds)
Expand source code
class SnapshotUnavailableReason(StrEnum):
    SNAPSHOT_UNSUPPORTED = 'SNAPSHOT_UNSUPPORTED'
    SNAPSHOT_TEMPORARILY_UNAVAILABLE = 'SNAPSHOT_TEMPORARILY_UNAVAILABLE'
    SNAPSHOT_PERMISSION_DENIED = 'SNAPSHOT_PERMISSION_DENIED'

Enum where members are also (and must be) strings

Ancestors

  • enum.StrEnum
  • builtins.str
  • enum.ReprEnum
  • enum.Enum

Class variables

var SNAPSHOT_PERMISSION_DENIED
var SNAPSHOT_TEMPORARILY_UNAVAILABLE
var SNAPSHOT_UNSUPPORTED
class MediaBuyDeliveryStatus (*args, **kwds)
Expand source code
class Status(StrEnum):
    pending_creatives = 'pending_creatives'
    pending_start = 'pending_start'
    pending = 'pending'
    active = 'active'
    paused = 'paused'
    completed = 'completed'
    rejected = 'rejected'
    canceled = 'canceled'
    failed = 'failed'
    reporting_delayed = 'reporting_delayed'

Enum where members are also (and must be) strings

Ancestors

  • enum.StrEnum
  • builtins.str
  • enum.ReprEnum
  • enum.Enum

Class variables

var active
var canceled
var completed
var failed
var paused
var pending
var pending_creatives
var pending_start
var rejected
var reporting_delayed
class SyncAccountsRequest (**data: Any)
Expand source code
class SyncAccountsRequest(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    idempotency_key: Annotated[
        str,
        Field(
            description='Client-generated unique key for at-most-once execution. Natural per-account upsert keys handle resource-level dedup, but the envelope triggers onboarding webhooks, billing setup, and audit events — this key prevents those side effects from firing twice on retry. MUST be unique per (seller, request) pair. Use a fresh UUID v4 for each request.',
            max_length=255,
            min_length=16,
            pattern='^[A-Za-z0-9_.:-]{16,255}$',
        ),
    ]
    accounts: Annotated[
        list[Accounts | Accounts1],
        Field(
            description='Per-account sync entries. Each entry uses one of two key shapes: the `account` field (AccountRef) for settings-update mode, or the flat `brand` + `operator` + `billing` trio for provisioning mode. An operator_identity settings update MUST carry the latest account revision.',
            max_length=1000,
        ),
    ]
    delete_missing: Annotated[
        bool | None,
        Field(
            description='When true, accounts previously synced by this agent but not included in this request will be deactivated. Scoped to the authenticated agent — does not affect accounts managed by other agents. Use with caution.'
        ),
    ] = False
    dry_run: Annotated[
        bool | None,
        Field(
            description='When true, preview what would change without applying. Returns what would be created/updated/deactivated.'
        ),
    ] = False
    push_notification_config: Annotated[
        push_notification_config_1.PushNotificationConfig | None,
        Field(
            description='Webhook for async notifications when account status changes (e.g., pending_approval transitions to active).'
        ),
    ] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var accounts : list[adcp.types.generated_poc.account.sync_accounts_request.Accounts | adcp.types.generated_poc.account.sync_accounts_request.Accounts1]
var context : adcp.types.generated_poc.core.context.ContextObject | None
var delete_missing : bool | None
var dry_run : bool | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var idempotency_key : str
var model_config
var push_notification_config : adcp.types.generated_poc.core.push_notification_config.PushNotificationConfig | None

Inherited members

class SyncAccountsSuccessResponse (**data: Any)
Expand source code
class SyncAccountsResponse1(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    dry_run: bool | None = None
    accounts: list[Account]
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var accounts : list[adcp.types.generated_poc.account.sync_accounts_response.Account]
var context : adcp.types.generated_poc.core.context.ContextObject | None
var dry_run : bool | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config
class SyncAccountsResponse1 (**data: Any)
Expand source code
class SyncAccountsResponse1(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    dry_run: bool | None = None
    accounts: list[Account]
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var accounts : list[adcp.types.generated_poc.account.sync_accounts_response.Account]
var context : adcp.types.generated_poc.core.context.ContextObject | None
var dry_run : bool | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config

Inherited members

class SyncAccountsErrorResponse (**data: Any)
Expand source code
class SyncAccountsResponse2(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    errors: Annotated[list[error_1.Error], Field(min_length=1)]
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var errors : list[adcp.types.generated_poc.core.error.Error]
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config

Inherited members

class SyncAgentNotificationConfigsRequest (**data: Any)
Expand source code
class SyncAgentNotificationConfigsRequest(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    idempotency_key: Annotated[
        str,
        Field(
            description="Client-generated unique key for at-most-once execution. The task mutates the authenticated caller's persistent agent-level subscriber set and may trigger endpoint proof-of-control challenges, so retries MUST reuse the same key and same payload. Use a fresh UUID v4 for each logical replacement.",
            max_length=255,
            min_length=16,
            pattern='^[A-Za-z0-9_.:-]{16,255}$',
        ),
    ]
    notification_configs: Annotated[
        list[agent_notification_config.AgentNotificationConfig],
        Field(
            description="Complete desired set of agent-level notification subscribers for the authenticated caller or registry identity. Omit is invalid; send an empty array to remove every subscriber owned by this caller. Each entry registers a URL, the agent-level event types the subscriber wants, and optional legacy auth. Duplicate `subscriber_id` values are rejected within this caller-scoped array. If any entry fails validation or activation proof, the seller rejects the replacement and leaves this caller's previous set unchanged.",
            max_length=16,
        ),
    ]
    dry_run: Annotated[
        bool | None,
        Field(
            description='When true, validate the replacement and report what would be persisted without applying it or sending endpoint proof-of-control challenges.'
        ),
    ] = False
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var dry_run : bool | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var idempotency_key : str
var model_config
var notification_configs : list[adcp.types.generated_poc.core.agent_notification_config.AgentNotificationConfig]

Inherited members

class SyncAgentNotificationConfigsResponse (**data: Any)
Expand source code
class SyncAgentNotificationConfigsResponse(AdcpVersionEnvelope, ProtocolEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    dry_run: Annotated[
        bool | None,
        Field(description='Whether this was a dry run and no persisted subscriber set changed.'),
    ] = None
    action: Annotated[
        Action,
        Field(
            description="Outcome for the caller-scoped replacement. `updated`: a new subscriber set was persisted for this caller. `unchanged`: the submitted set matched this caller's current set. `cleared`: the submitted empty array removed all subscribers owned by this caller. `failed`: validation, authorization, or endpoint proof failed; the prior caller-scoped set remains unchanged and `errors[]` explains why."
        ),
    ]
    notification_configs: Annotated[
        list[agent_notification_config.AgentNotificationConfig] | None,
        Field(
            description='Current persisted agent-level notification subscribers owned by the authenticated caller after the request. Entries are keyed by `subscriber_id`; `authentication.credentials` is omitted on every entry because credentials are write-only. Present on successful actions and MAY be present on `failed` responses to show the unchanged prior caller-scoped set.',
            max_length=16,
        ),
    ] = None
    errors: Annotated[
        list[error.Error] | None,
        Field(
            description='Operation-level errors and warnings. Present when action is `failed`, and MAY include non-fatal warnings on successful responses.'
        ),
    ] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • adcp.types.generated_poc.core.protocol_envelope.ProtocolEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var action : adcp.types.generated_poc.protocol.sync_agent_notification_configs_response.Action
var context : adcp.types.generated_poc.core.context.ContextObject | None
var dry_run : bool | None
var errors : list[adcp.types.generated_poc.core.error.Error] | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config
var notification_configs : list[adcp.types.generated_poc.core.agent_notification_config.AgentNotificationConfig] | None

Inherited members

class SyncAudiencesRequest (**data: Any)
Expand source code
class SyncAudiencesRequest(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    idempotency_key: Annotated[
        str,
        Field(
            description='Client-generated unique key for at-most-once execution. `audience_id` gives resource-level dedup per audience, but the sync envelope emits audit events and may trigger downstream refreshes — this key prevents those side effects from firing twice on retry. Also serves as a request ID on discovery-only calls (when `audiences` is omitted). MUST be unique per (seller, request) pair. Use a fresh UUID v4 for each request.',
            max_length=255,
            min_length=16,
            pattern='^[A-Za-z0-9_.:-]{16,255}$',
        ),
    ]
    account: Annotated[
        account_ref.AccountReference, Field(description='Account to manage audiences for.')
    ]
    audiences: Annotated[
        list[Audience] | None,
        Field(
            description='Audiences to sync (create or update). When omitted, the call is discovery-only and returns all existing audiences on the account without modification.',
            min_length=1,
        ),
    ] = None
    delete_missing: Annotated[
        bool | None,
        Field(
            description='When true, buyer-managed audiences on the account not included in this sync will be removed. Does not affect seller-managed audiences. Do not combine with an omitted audiences array or all buyer-managed audiences will be deleted.'
        ),
    ] = False
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var account : adcp.types.generated_poc.core.account_ref.AccountReference
var audiences : list[adcp.types.generated_poc.media_buy.sync_audiences_request.Audience] | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var delete_missing : bool | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var idempotency_key : str
var model_config

Inherited members

class SyncAudiencesSuccessResponse (**data: Any)
Expand source code
class SyncAudiencesResponse1(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    audiences: list[Audience]
    sandbox: bool | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var audiences : list[adcp.types.generated_poc.media_buy.sync_audiences_response.Audience]
var context : adcp.types.generated_poc.core.context.ContextObject | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config
var sandbox : bool | None
class SyncAudiencesResponse1 (**data: Any)
Expand source code
class SyncAudiencesResponse1(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    audiences: list[Audience]
    sandbox: bool | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var audiences : list[adcp.types.generated_poc.media_buy.sync_audiences_response.Audience]
var context : adcp.types.generated_poc.core.context.ContextObject | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config
var sandbox : bool | None

Inherited members

class SyncAudiencesErrorResponse (**data: Any)
Expand source code
class SyncAudiencesResponse2(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    errors: Annotated[list[error_1.Error], Field(min_length=1)]
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var errors : list[adcp.types.generated_poc.core.error.Error]
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config

Inherited members

class SyncAudiencesSubmittedResponse (**data: Any)
Expand source code
class SyncAudiencesResponse3(AdcpVersionEnvelope, ProtocolEnvelope):
    model_config = ConfigDict(extra='allow', validate_default=True)
    status: Literal[task_status_1.TaskStatus.submitted] = task_status_1.TaskStatus.submitted
    task_id: str
    message: Annotated[str, StringConstraints(max_length=2000)] | None = None
    errors: list[error_1.Error] | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • adcp.types.generated_poc.core.protocol_envelope.ProtocolEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var errors : list[adcp.types.generated_poc.core.error.Error] | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var message : str | None
var model_config
var status : Literal[<TaskStatus.submitted: 'submitted'>]
var task_id : str

Inherited members

class SyncCatalogsInputRequired (**data: Any)
Expand source code
class SyncCatalogsInputRequired(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    reason: Annotated[
        Reason | None,
        Field(
            description='Reason code indicating why buyer input is needed. APPROVAL_REQUIRED: platform requires explicit approval before activating the catalog. FEED_VALIDATION: feed URL returned unexpected format or schema errors. ITEM_REVIEW: platform flagged items for manual review. FEED_ACCESS: platform cannot access the feed URL (authentication, CORS, etc.).'
        ),
    ] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config
var reason : adcp.types.generated_poc.media_buy.sync_catalogs_async_response_input_required.Reason | None

Inherited members

class SyncCatalogsRequest (**data: Any)
Expand source code
class SyncCatalogsRequest(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    idempotency_key: Annotated[
        str,
        Field(
            description='Client-generated unique key for at-most-once execution. Catalog upserts and item availability transitions can emit audit events or trigger platform work — this key prevents those side effects from firing twice on retry. Also serves as a request ID on discovery-only calls. MUST be unique per (seller, request) pair. Use a fresh UUID v4 for each request.',
            max_length=255,
            min_length=16,
            pattern='^[A-Za-z0-9_.:-]{16,255}$',
        ),
    ]
    account: Annotated[
        account_ref.AccountReference,
        Field(description='Seller account containing these buyer-managed catalogs.'),
    ]
    catalogs: Annotated[
        list[catalog.Catalog] | None,
        Field(
            description='Array of catalog feeds to sync (create or update). When omitted together with item_availability_updates and item_availability_queries, the call is discovery-only and returns all existing catalogs on the account without modification.',
            max_length=50,
            min_length=1,
        ),
    ] = None
    item_availability_updates: Annotated[
        list[catalog_item_availability_update.CatalogItemAvailabilityUpdate] | None,
        Field(
            description='Immediate suppress or restore operations for items in buyer-managed catalogs. Sellers declaring media_buy.features.catalog_item_availability_updates MUST process these updates synchronously and MUST NOT silently ignore them or return a submitted task. A seller that does not declare the capability MUST reject the request with UNSUPPORTED_FEATURE before lookup or mutation and MUST NOT interpret it as discovery. The combined number of item_availability_updates and item_availability_queries MUST NOT exceed 1,000; excess entries are an operation-level INVALID_REQUEST before lookup or mutation. Each (catalog_id, catalog_generation, item_id) tuple MUST appear at most once in updates; a duplicate is an operation-level INVALID_REQUEST before mutation in every validation mode. For mixed catalog/update requests, the seller MUST validate and stage the entire request against the post-upsert candidate state, then commit catalog and availability changes atomically. It MUST reject before any mutation if synchronous atomic commit is unavailable. A successful suppress acknowledgement means the seller MUST stop selecting or rendering the item and every cached or pre-generated creative it materialized from the item. Seller-internal generation lineage MUST retain resolved_account_id, catalog_id, catalog_generation, and item_id. If the seller cannot enforce that guarantee, it MUST return a failed per-item result. Suppression persists across scheduled feed fetches and catalog upserts until explicit restore, expires_at, or deletion of the containing catalog. Restore removes only an existing buyer-authored overlay or tombstone in the same catalog generation and cannot override seller rejection, withdrawal, policy, rights, or inventory controls. A restore for an absent item without such prior state fails with REFERENCE_NOT_FOUND.',
            max_length=1000,
            min_length=1,
        ),
    ] = None
    item_availability_queries: Annotated[
        list[catalog_item_availability_ref.CatalogItemAvailabilityReference] | None,
        Field(
            description='Read current buyer-authored availability state. Queries require media_buy.features.catalog_item_availability_updates; a seller that does not declare it rejects with UNSUPPORTED_FEATURE before lookup. Any request containing queries is synchronous. In a mixed request the seller validates and stages catalog upserts and availability updates first, evaluates queries against that post-upsert/post-update candidate state, and atomically commits the staged mutations before returning those query results. If the mixed work cannot commit synchronously, it rejects before mutation. The seller returns exactly one item_availability_states entry per query in the same order and echoes request_index and the complete identity. Unknown, inaccessible, stale-generation, and unauthorized references use the normalized REFERENCE_NOT_FOUND shape described by validation_mode. Use a fresh idempotency_key for a current read; a replayed response is a historical snapshot.',
            max_length=1000,
            min_length=1,
        ),
    ] = None
    catalog_ids: Annotated[
        list[str] | None,
        Field(
            description='Optional filter to limit sync scope to specific catalog IDs. When provided, only these catalogs will be created/updated. Other catalogs on the account are unaffected.',
            max_length=50,
            min_length=1,
        ),
    ] = None
    delete_missing: Annotated[
        bool | None,
        Field(
            description='When true, buyer-managed catalogs on the account not included in this sync will be removed. Does not affect seller-managed catalogs. Requires catalogs; item_availability_updates alone cannot define deletion scope.'
        ),
    ] = False
    dry_run: Annotated[
        bool | None,
        Field(
            description='When true, preview catalog create, update, and delete changes without applying them. MUST NOT be combined with item_availability_updates.'
        ),
    ] = False
    validation_mode: Annotated[
        validation_mode_1.ValidationMode | None,
        Field(
            description="Validation strictness for semantically valid-looking catalog and item entries. In strict mode (default), an unknown, inaccessible, unauthorized, or stale-generation catalog/item reference, a known seller-managed catalog, a stale expected_overlay_revision, or another per-entry error fails the entire operation before any catalog or availability mutation. In lenient mode, the seller returns a positionally matched failed result for each such item entry and processes the remaining valid entries. Unknown, inaccessible, unauthorized, and stale-generation references MUST be observationally equivalent: code REFERENCE_NOT_FOUND, message exactly 'Catalog item not found', recovery 'correctable', and no field, suggestion, retry_after, issues, details, or resource metadata. Authorization and lookup MUST use the same externally observable failure path and SHOULD avoid materially distinguishable timing. A known seller-managed catalog may use INVALID_REQUEST only after catalog access is authorized. Request-schema failures, duplicate identity tuples, unsupported capability, batch-limit excess, dry_run conflicts, and mixed requests that cannot commit synchronously and atomically are operation-level failures before lookup or mutation in both modes. A stale revision uses CONFLICT without mutation."
        ),
    ] = validation_mode_1.ValidationMode.strict
    push_notification_config: Annotated[
        push_notification_config_1.PushNotificationConfig | None,
        Field(
            description='Optional webhook configuration for async sync notifications. Publisher will send webhook when sync completes if operation takes longer than immediate response time (common for large feeds requiring platform review).'
        ),
    ] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var account : adcp.types.generated_poc.core.account_ref.AccountReference
var catalog_ids : list[str] | None
var catalogs : list[adcp.types.generated_poc.core.catalog.Catalog] | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var delete_missing : bool | None
var dry_run : bool | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var idempotency_key : str
var item_availability_queries : list[adcp.types.generated_poc.core.catalog_item_availability_ref.CatalogItemAvailabilityReference] | None
var item_availability_updates : list[adcp.types.generated_poc.core.catalog_item_availability_update.CatalogItemAvailabilityUpdate] | None
var model_config
var push_notification_config : adcp.types.generated_poc.core.push_notification_config.PushNotificationConfig | None
var validation_mode : adcp.types.generated_poc.enums.validation_mode.ValidationMode | None

Inherited members

class SyncCatalogsSuccessResponse (**data: Any)
Expand source code
class SyncCatalogsResponse1(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    status: Literal['completed'] | None = None
    dry_run: bool | None = None
    catalogs: list[Catalog]
    item_availability_updates: Annotated[list[catalog_item_availability_update_result_1.CatalogItemAvailabilityUpdateResult], Field(min_length=1, max_length=1000)] | None = None
    item_availability_states: Annotated[list[catalog_item_availability_state_1.CatalogItemAvailabilityState], Field(min_length=1, max_length=1000)] | None = None
    sandbox: bool | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var catalogs : list[adcp.types.generated_poc.media_buy.sync_catalogs_response.Catalog]
var context : adcp.types.generated_poc.core.context.ContextObject | None
var dry_run : bool | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var item_availability_states : list[adcp.types.generated_poc.core.catalog_item_availability_state.CatalogItemAvailabilityState] | None
var item_availability_updates : list[adcp.types.generated_poc.core.catalog_item_availability_update_result.CatalogItemAvailabilityUpdateResult] | None
var model_config
var sandbox : bool | None
var status : Literal['completed'] | None
class SyncCatalogsResponse1 (**data: Any)
Expand source code
class SyncCatalogsResponse1(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    status: Literal['completed'] | None = None
    dry_run: bool | None = None
    catalogs: list[Catalog]
    item_availability_updates: Annotated[list[catalog_item_availability_update_result_1.CatalogItemAvailabilityUpdateResult], Field(min_length=1, max_length=1000)] | None = None
    item_availability_states: Annotated[list[catalog_item_availability_state_1.CatalogItemAvailabilityState], Field(min_length=1, max_length=1000)] | None = None
    sandbox: bool | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var catalogs : list[adcp.types.generated_poc.media_buy.sync_catalogs_response.Catalog]
var context : adcp.types.generated_poc.core.context.ContextObject | None
var dry_run : bool | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var item_availability_states : list[adcp.types.generated_poc.core.catalog_item_availability_state.CatalogItemAvailabilityState] | None
var item_availability_updates : list[adcp.types.generated_poc.core.catalog_item_availability_update_result.CatalogItemAvailabilityUpdateResult] | None
var model_config
var sandbox : bool | None
var status : Literal['completed'] | None

Inherited members

class SyncCatalogsErrorResponse (**data: Any)
Expand source code
class SyncCatalogsResponse2(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    errors: Annotated[list[Any], Field(min_length=1)]
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var errors : list[typing.Any]
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config

Inherited members

class SyncCatalogsSubmittedResponse (**data: Any)
Expand source code
class SyncCatalogsResponse3(AdcpVersionEnvelope, ProtocolEnvelope):
    model_config = ConfigDict(extra='allow', validate_default=True)
    status: Literal[task_status_1.TaskStatus.submitted] = task_status_1.TaskStatus.submitted
    task_id: str
    message: Annotated[str, StringConstraints(max_length=2000)] | None = None
    errors: list[error_1.Error] | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • adcp.types.generated_poc.core.protocol_envelope.ProtocolEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var errors : list[adcp.types.generated_poc.core.error.Error] | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var message : str | None
var model_config
var status : Literal[<TaskStatus.submitted: 'submitted'>]
var task_id : str

Inherited members

class SyncCatalogsSubmitted (**data: Any)
Expand source code
class SyncCatalogsSubmitted(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    status: Annotated[
        Literal['submitted'],
        Field(
            description='Task-level status literal. Discriminates this async envelope from the synchronous success shape, whose catalogs array is issued in-line. See task-status.json for the full task-status enum.'
        ),
    ] = 'submitted'
    task_id: Annotated[
        str,
        Field(
            description='Task handle the buyer uses with get_task_status (or the legacy AdCP tasks/get alias), and that the seller references on push-notification callbacks. This AdCP application-layer handle remains the snake_case task_id in every transport payload and is distinct from any transport-native A2A Task id.'
        ),
    ]
    message: Annotated[
        str | None,
        Field(
            description="Optional human-readable explanation of why the task is submitted — e.g., 'Catalog ingestion queued; typical turnaround 5–15 minutes.' Plain text only. Buyers MUST treat this as untrusted seller input: escape before rendering to HTML UIs, and sanitize or isolate before passing to an LLM prompt context — a hostile seller may inject prompt-injection payloads aimed at the buyer's agent.",
            max_length=2000,
        ),
    ] = None
    errors: Annotated[
        list[error.Error] | None,
        Field(
            description='Optional advisory errors accompanying the submitted envelope. Use only for non-blocking warnings (e.g., throttled_severity advisories, governance observations). Terminal failures belong in the error branch, not here.'
        ),
    ] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var errors : list[adcp.types.generated_poc.core.error.Error] | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var message : str | None
var model_config
var status : Literal['submitted']
var task_id : str

Inherited members

class SyncCatalogsWorking (**data: Any)
Expand source code
class SyncCatalogsWorking(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    percentage: Annotated[
        float | None, Field(description='Completion percentage (0-100)', ge=0.0, le=100.0)
    ] = None
    current_step: Annotated[
        str | None,
        Field(
            description="Current step or phase of the operation (e.g., 'Fetching product feed', 'Validating items', 'Platform review')"
        ),
    ] = None
    total_steps: Annotated[
        int | None, Field(description='Total number of steps in the operation', ge=1)
    ] = None
    step_number: Annotated[int | None, Field(description='Current step number', ge=1)] = None
    catalogs_processed: Annotated[
        int | None, Field(description='Number of catalogs processed so far', ge=0)
    ] = None
    catalogs_total: Annotated[
        int | None, Field(description='Total number of catalogs to process', ge=0)
    ] = None
    items_processed: Annotated[
        int | None,
        Field(description='Total number of catalog items processed across all catalogs', ge=0),
    ] = None
    items_total: Annotated[
        int | None,
        Field(description='Total number of catalog items to process across all catalogs', ge=0),
    ] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var catalogs_processed : int | None
var catalogs_total : int | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var current_step : str | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var items_processed : int | None
var items_total : int | None
var model_config
var percentage : float | None
var step_number : int | None
var total_steps : int | None

Inherited members

class SyncCreativesRequest (**data: Any)

Base class enforcing the primary canonical runtime boundary.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var account : adcp.types.generated_poc.core.account_ref.AccountReference
var adcp_major_version : int | None
var adcp_version : str | None
var assignment_operations : list[adcp.types.generated_poc.creative.sync_creatives_request.AssignmentOperations] | None
var assignments : list[adcp.types.generated_poc.creative.sync_creatives_request.Assignment] | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var creative_ids : list[str] | None
var creatives : list[CreativeAsset]
var delete_missing : bool | None
var dry_run : bool | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var idempotency_key : str
var model_config
var push_notification_config : adcp.types.generated_poc.core.push_notification_config.PushNotificationConfig | None
var validation_mode : adcp.types.generated_poc.enums.validation_mode.ValidationMode | None
class LegacySyncCreativesRequest (**data: Any)
Expand source code
class SyncCreativesRequest(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    account: Annotated[
        account_ref.AccountReference, Field(description='Account that owns these creatives.')
    ]
    creatives: Annotated[
        list[Creative] | None,
        Field(
            description='Array of creative assets to sync (create or update)',
            max_length=100,
            min_length=1,
        ),
    ] = None
    creative_ids: Annotated[
        list[str] | None,
        Field(
            description='Optional filter to limit sync scope to specific creative IDs. When provided, only these creatives will be created/updated. Other creatives in the library are unaffected. Useful for partial updates and error recovery.',
            max_length=100,
            min_length=1,
        ),
    ] = None
    assignments: Annotated[
        list[Assignment] | None,
        Field(
            deprecated=True,
            description='Deprecated additive assignment shorthand. Each entry upserts one creative-to-package assignment. Use assignment_operations for explicit assign, unassign, and replace semantics. Standalone creative agents that do not manage media buys ignore this field.',
            min_length=1,
        ),
    ] = None
    assignment_operations: Annotated[
        list[AssignmentOperations] | None,
        Field(
            description='Explicit, ordered assignment mutations. These operations may be sent without creatives to traffic existing creative IDs independently from MediaBuy commercial control. The entire request is atomic under idempotency_key and therefore requires strict validation; lenient partial processing is not permitted.',
            max_length=500,
            min_length=1,
        ),
    ] = None
    idempotency_key: Annotated[
        str,
        Field(
            description='Client-generated idempotency key for safe retries. If a sync fails without a response, resending with the same idempotency_key guarantees at-most-once execution. MUST be unique per (seller, request) pair to prevent cross-seller correlation. Use a fresh UUID v4 for each request.',
            max_length=255,
            min_length=16,
            pattern='^[A-Za-z0-9_.:-]{16,255}$',
        ),
    ]
    delete_missing: Annotated[
        bool | None,
        Field(
            description='When true, creatives not included in this sync will be archived. Use with caution for full library replacement. Invalid when creative_ids is provided — delete_missing applies to the entire library scope, not a filtered subset.'
        ),
    ] = False
    dry_run: Annotated[
        bool | None,
        Field(
            description="When true, rehearse this sync_creatives operation without applying it. Validates the actual trafficking request in the seller's current context, including library upsert semantics, creative IDs, assignments, account-scoped gates, and seller policies, then returns what would be created/updated/deleted. This is distinct from validate_input, which only validates manifest structure against canonical/product format targets."
        ),
    ] = False
    validation_mode: Annotated[
        validation_mode_1.ValidationMode | None,
        Field(
            description="Validation strictness. 'strict' fails entire sync on any validation error. 'lenient' processes valid creatives and reports errors."
        ),
    ] = validation_mode_1.ValidationMode.strict
    push_notification_config: Annotated[
        push_notification_config_1.PushNotificationConfig | None,
        Field(
            description='Optional webhook configuration for async sync notifications. The agent will send a webhook when sync completes if the operation takes longer than immediate response time (typically for large bulk operations or manual approval/HITL).'
        ),
    ] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var account : adcp.types.generated_poc.core.account_ref.AccountReference
var assignment_operations : list[adcp.types.generated_poc.creative.sync_creatives_request.AssignmentOperations] | None
var assignments : list[adcp.types.generated_poc.creative.sync_creatives_request.Assignment] | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var creative_ids : list[str] | None
var creatives : list[adcp.types.generated_poc.creative.sync_creatives_request.Creative] | None
var delete_missing : bool | None
var dry_run : bool | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var idempotency_key : str
var model_config
var push_notification_config : adcp.types.generated_poc.core.push_notification_config.PushNotificationConfig | None
var validation_mode : adcp.types.generated_poc.enums.validation_mode.ValidationMode | None

Inherited members

class SyncCreativesSuccessResponse (**data: Any)
Expand source code
class SyncCreativesResponse1(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    dry_run: bool | None = None
    creatives: list[Creative]
    sandbox: bool | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var creatives : list[adcp.types.generated_poc.creative.sync_creatives_response.Creative]
var dry_run : bool | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config
var sandbox : bool | None
class SyncCreativesResponse1 (**data: Any)
Expand source code
class SyncCreativesResponse1(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    dry_run: bool | None = None
    creatives: list[Creative]
    sandbox: bool | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var creatives : list[adcp.types.generated_poc.creative.sync_creatives_response.Creative]
var dry_run : bool | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config
var sandbox : bool | None

Inherited members

class SyncCreativesErrorResponse (**data: Any)
Expand source code
class SyncCreativesResponse2(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    errors: Annotated[list[error_1.Error], Field(min_length=1)]
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var errors : list[adcp.types.generated_poc.core.error.Error]
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config

Inherited members

class SyncCreativesResponse3 (**data: Any)
Expand source code
class SyncCreativesResponse3(AdcpVersionEnvelope, ProtocolEnvelope):
    model_config = ConfigDict(extra='allow', validate_default=True)
    status: Literal[task_status_1.TaskStatus.submitted] = task_status_1.TaskStatus.submitted
    task_id: str
    message: Annotated[str, StringConstraints(max_length=2000)] | None = None
    errors: list[error_1.Error] | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • adcp.types.generated_poc.core.protocol_envelope.ProtocolEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var errors : list[adcp.types.generated_poc.core.error.Error] | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var message : str | None
var model_config
var status : Literal[<TaskStatus.submitted: 'submitted'>]
var task_id : str
class SyncCreativesSubmittedResponse (**data: Any)
Expand source code
class SyncCreativesResponse3(AdcpVersionEnvelope, ProtocolEnvelope):
    model_config = ConfigDict(extra='allow', validate_default=True)
    status: Literal[task_status_1.TaskStatus.submitted] = task_status_1.TaskStatus.submitted
    task_id: str
    message: Annotated[str, StringConstraints(max_length=2000)] | None = None
    errors: list[error_1.Error] | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • adcp.types.generated_poc.core.protocol_envelope.ProtocolEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var errors : list[adcp.types.generated_poc.core.error.Error] | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var message : str | None
var model_config
var status : Literal[<TaskStatus.submitted: 'submitted'>]
var task_id : str

Inherited members

class SyncEventSourcesRequest (**data: Any)
Expand source code
class SyncEventSourcesRequest(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    idempotency_key: Annotated[
        str,
        Field(
            description='Client-generated unique key for at-most-once execution. `event_source_id` gives resource-level dedup per source, but the sync envelope emits audit events and can trigger downstream pixel provisioning — this key prevents those side effects from firing twice on retry. Also serves as a request ID on discovery-only calls (when `event_sources` is omitted). MUST be unique per (seller, request) pair. Use a fresh UUID v4 for each request.',
            max_length=255,
            min_length=16,
            pattern='^[A-Za-z0-9_.:-]{16,255}$',
        ),
    ]
    account: Annotated[
        account_ref.AccountReference, Field(description='Account to configure event sources for.')
    ]
    event_sources: Annotated[
        list[EventSource] | None,
        Field(
            description='Event sources to sync (create or update). When omitted, the call is discovery-only and returns all existing event sources on the account without modification.',
            min_length=1,
        ),
    ] = None
    delete_missing: Annotated[
        bool | None,
        Field(description='When true, event sources not included in this sync will be removed'),
    ] = False
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var account : adcp.types.generated_poc.core.account_ref.AccountReference
var context : adcp.types.generated_poc.core.context.ContextObject | None
var delete_missing : bool | None
var event_sources : list[adcp.types.generated_poc.media_buy.sync_event_sources_request.EventSource] | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var idempotency_key : str
var model_config

Inherited members

class SyncEventSourcesSuccessResponse (**data: Any)
Expand source code
class SyncEventSourcesResponse1(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    event_sources: list[EventSource]
    sandbox: bool | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var event_sources : list[adcp.types.generated_poc.media_buy.sync_event_sources_response.EventSource]
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config
var sandbox : bool | None
class SyncEventSourcesResponse1 (**data: Any)
Expand source code
class SyncEventSourcesResponse1(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    event_sources: list[EventSource]
    sandbox: bool | None = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var event_sources : list[adcp.types.generated_poc.media_buy.sync_event_sources_response.EventSource]
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config
var sandbox : bool | None

Inherited members

class SyncEventSourcesErrorResponse (**data: Any)
Expand source code
class SyncEventSourcesResponse2(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    errors: Annotated[list[error_1.Error], Field(min_length=1)]
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var errors : list[adcp.types.generated_poc.core.error.Error]
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config

Inherited members

class SyncPlansRequest (**data: Any)
Expand source code
class SyncPlansRequest(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    idempotency_key: Annotated[
        str,
        Field(
            description='Client-generated unique key for at-most-once execution. `plan_id` gives resource-level dedup per plan, but the sync envelope emits audit events and can trigger governance reapproval — this key prevents those side effects from firing twice on retry. MUST be unique per (seller, request) pair. Use a fresh UUID v4 for each request.',
            max_length=255,
            min_length=16,
            pattern='^[A-Za-z0-9_.:-]{16,255}$',
        ),
    ]
    plans: Annotated[list[Plan], Field(description='One or more campaign plans to sync.')]
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var idempotency_key : str
var model_config
var plans : list[adcp.types.generated_poc.governance.sync_plans_request.Plan]

Inherited members

class SyncPlansResponse (**data: Any)
Expand source code
class SyncPlansResponse(AdcpVersionEnvelope, ProtocolEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    plans: Annotated[list[Plan], Field(description='Status for each synced plan.')]
    replayed: Annotated[
        bool | None,
        Field(
            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."
        ),
    ] = False
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • adcp.types.generated_poc.core.protocol_envelope.ProtocolEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config
var plans : list[adcp.types.generated_poc.governance.sync_plans_response.Plan]
var replayed : bool | None

Inherited members

class TargetingOverlay (**data: Any)
Expand source code
class TargetingOverlay(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    geo_countries: Annotated[
        list[GeoCountry] | None,
        Field(
            description="Restrict delivery to specific countries. ISO 3166-1 alpha-2 codes (e.g., 'US', 'GB', 'DE').",
            min_length=1,
        ),
    ] = None
    geo_countries_exclude: Annotated[
        Sequence[GeoCountriesExcludeItem] | None,
        Field(
            description="Exclude specific countries from delivery. ISO 3166-1 alpha-2 codes (e.g., 'US', 'GB', 'DE').",
            min_length=1,
        ),
    ] = None
    geo_regions: Annotated[
        list[GeoRegion] | None,
        Field(
            description='Restrict delivery to exact canonical ISO 3166-2 subdivisions (states, provinces, regions, departments, or other subdivision categories). Unknown identifiers are invalid. At create or update, sellers MUST reject unsupported identifiers and MUST NOT silently widen, drop, or partially apply the list. During get_products, a seller may instead return a sparse, buyer-reviewable targeting_resolution modification for a valid but unsupported requested outcome. Exact internal translation preserves accepted identifiers in package readback.',
            min_length=1,
        ),
    ] = None
    geo_regions_exclude: Annotated[
        Sequence[GeoRegionsExcludeItem] | None,
        Field(
            description='Exclude exact canonical ISO 3166-2 subdivisions. Support is independent from geo_regions inclusion support. Unknown identifiers and values also present in geo_regions are invalid. At create or update, sellers MUST reject unsupported identifiers and partial application; during get_products, a seller may instead return a sparse, buyer-reviewable targeting_resolution modification for a valid but unsupported requested outcome.',
            min_length=1,
        ),
    ] = None
    geo_metros: Annotated[
        list[GeoMetro] | None,
        Field(
            description='Restrict delivery to specific metro areas. Each entry specifies the classification system and target values. Seller must declare supported systems in get_adcp_capabilities.',
            min_length=1,
        ),
    ] = None
    geo_metros_exclude: Annotated[
        Sequence[GeoMetrosExcludeItem] | None,
        Field(
            description='Exclude specific metro areas from delivery. Each entry specifies the classification system and excluded values. Seller must declare supported systems in get_adcp_capabilities.',
            min_length=1,
        ),
    ] = None
    geo_postal_areas: Annotated[
        list[postal_area.PostalArea] | None,
        Field(
            description='Restrict delivery to specific postal areas. Prefer the native country + postal system form. The deprecated legacy country-fused postal-system tokens remain accepted for compatibility. Seller must declare supported systems in get_adcp_capabilities.',
            min_length=1,
        ),
    ] = None
    geo_postal_areas_exclude: Annotated[
        Sequence[postal_area.PostalArea] | None,
        Field(
            description='Exclude specific postal areas from delivery. Prefer the native country + postal system form. The deprecated legacy country-fused postal-system tokens remain accepted for compatibility. Seller must declare supported systems in get_adcp_capabilities.',
            min_length=1,
        ),
    ] = None
    geo_places: Annotated[
        list[geo_place_area.GeographicPlaceArea] | None,
        Field(
            description='Restrict delivery to catalog-backed named places. Values MUST be stable identifiers in the declared system, not display names. Sellers must declare supported systems, countries, and place types in get_adcp_capabilities and reject unsupported entries rather than silently dropping them.',
            min_length=1,
        ),
    ] = None
    geo_places_exclude: Annotated[
        list[geo_place_area.GeographicPlaceArea] | None,
        Field(
            description='Exclude catalog-backed named places. Uses the same identifier-based shape as geo_places. Sellers MUST reject overlap with geo_places for the same country, system, place_type, and value.',
            min_length=1,
        ),
    ] = None
    daypart_targets: Annotated[
        list[daypart_target.DaypartTarget] | None,
        Field(
            description='Restrict delivery to specific time windows. Each entry specifies days of week and an hour range.',
            min_length=1,
        ),
    ] = None
    axe_include_segment: Annotated[
        str | None,
        Field(
            deprecated=True,
            description='Deprecated: Use TMP provider fields instead. AXE segment ID to include for targeting.',
        ),
    ] = None
    axe_exclude_segment: Annotated[
        str | None,
        Field(
            deprecated=True,
            description='Deprecated: Use TMP provider fields instead. AXE segment ID to exclude from targeting.',
        ),
    ] = None
    audience_include: Annotated[
        list[str] | None,
        Field(
            description='Restrict delivery to members of these first-party CRM audiences. Only users present in the uploaded lists are eligible. References audience_id values from sync_audiences on the same seller account — audience IDs are not portable across sellers. Not for lookalike expansion — express that intent in the campaign brief. Seller must declare support in get_adcp_capabilities.',
            min_length=1,
        ),
    ] = None
    audience_exclude: Annotated[
        list[str] | None,
        Field(
            description='Suppress delivery to members of these first-party CRM audiences. Matched users are excluded regardless of other targeting. References audience_id values from sync_audiences on the same seller account — audience IDs are not portable across sellers. Seller must declare support in get_adcp_capabilities.',
            min_length=1,
        ),
    ] = None
    signal_targeting_groups: Annotated[
        package_signal_targeting_groups.PackageSignalTargetingGroups | None,
        Field(
            description="Basic Boolean grouping for seller-offered signals. v1 supports a required top-level operator 'all' and child groups with operator 'any' for include groups or 'none' for exclusion groups. Example semantics: group 1 any(A, B) plus group 2 none(C, D) means (A OR B) AND NOT (C OR D). Signal entries reference named signal definitions with signal_ref scope 'product' for product-local signal options or scope 'data_provider' for external signals published in adagents.json signals[]. For simple include-only targeting, send one child group with operator 'any'. Sellers SHOULD reject entries that are not available for the product through inline signal_targeting_options or get_signals, are not active for the account, or exceed the product's signal_targeting_allowed/signal_targeting_rules/product terms. Signal targeting limits are product-scoped, not declared in get_adcp_capabilities, because products may be backed by different ad servers. Sellers MUST echo applied signal_targeting_groups on the resulting package state, including fixed/default selections. Sellers MAY return REQUOTE_REQUIRED when a targeting mutation changes commercial terms."
        ),
    ] = None
    signal_targeting: Annotated[
        list[signal_targeting_1.SignalTargeting] | None,
        Field(
            deprecated=True,
            description='DEPRECATED. Use signal_targeting_groups for package-level signal targeting. Legacy flat signal_targeting remains accepted during the SignalRef migration window but cannot express grouped include/exclude composition or product-scoped pricing.',
            min_length=1,
        ),
    ] = None
    demographics: Annotated[
        demographic_targeting_intent.DemographicTargetingIntent | None,
        Field(
            description='Canonical demographic audience targeting intent with optional constraints on how age may be determined. This is distinct from age_restriction: demographics selects an audience, while age_restriction expresses a legal eligibility or verification floor. Fresh create/update targeting MUST compile exactly or be rejected. During get_products, a seller may offer a different configured predicate only through sparse targeting_resolution modifications on a distinguishable product_id; selecting that product accepts the alternative. Sellers never silently broaden, narrow, default, drop, or substitute the basis.'
        ),
    ] = None
    frequency_cap: frequency_cap_1.FrequencyCap | None = None
    property_list: Annotated[
        property_list_ref.PropertyListReference | None,
        Field(
            description="Reference to a property list for targeting specific properties within this product. The package runs on the intersection of the product's publisher_properties and this list. Sellers SHOULD return a validation error if the product has property_targeting_allowed: false."
        ),
    ] = None
    property_list_exclude: Annotated[
        property_list_ref.PropertyListReference | None,
        Field(
            description="Reference to a property list whose properties must not carry the buyer's ads. Matched properties are removed from delivery. Use for brand-safety do-not-run lists (apps, sites). Exclude wins on overlap with property_list, and applies regardless of the product's property_targeting_allowed flag. Seller must declare support in get_adcp_capabilities."
        ),
    ] = None
    collection_list: Annotated[
        collection_list_ref.CollectionListReference | None,
        Field(
            description='Reference to a collection list for including specific collections (programs, shows) within this product. The package runs on the intersection of matched collections and this list. Use for inclusion-based collection targeting. Seller must declare support in get_adcp_capabilities.'
        ),
    ] = None
    collection_list_exclude: Annotated[
        collection_list_ref.CollectionListReference | None,
        Field(
            description="Reference to a collection list for excluding specific collections (programs, shows) from this product. Matched collections must not carry the buyer's ads. Use for brand safety do-not-air lists. Seller must declare support in get_adcp_capabilities."
        ),
    ] = None
    placement_selection: Annotated[
        placement_selection_1.PlacementSelection | None,
        Field(
            description='Purchased placement selection within the product. This constrains package inventory; it is distinct from creative_assignments[].placement_refs, which only route individual creatives within the purchased set. On create, mode selected supplies the complete selected set and mode default uses the product default. On update, the surrounding targeting_overlay replacement semantics apply.'
        ),
    ] = None
    age_restriction: Annotated[
        AgeRestriction | None,
        Field(
            description='Age restriction for compliance. Use for legal requirements (alcohol, gambling), not audience targeting.'
        ),
    ] = None
    device_platform: Annotated[
        list[device_platform_1.DevicePlatform] | None,
        Field(
            description='Restrict to specific platforms. Use for technical compatibility (app only works on iOS). Values from Sec-CH-UA-Platform standard, extended for CTV.',
            min_length=1,
        ),
    ] = None
    device_platform_exclude: Annotated[
        list[device_platform_1.DevicePlatform] | None,
        Field(
            description='Exclude specific operating-system platforms from delivery. When a platform appears in both device_platform and device_platform_exclude, exclusion wins. Sellers MUST reject a request they cannot enforce rather than silently dropping the exclusion.',
            min_length=1,
        ),
    ] = None
    device_type: Annotated[
        list[device_type_1.DeviceType] | None,
        Field(
            description='Restrict to specific device form factors. Use for campaigns targeting hardware categories rather than operating systems (e.g., mobile-only promotions, CTV campaigns).',
            min_length=1,
        ),
    ] = None
    device_type_exclude: Annotated[
        list[device_type_1.DeviceType] | None,
        Field(
            description='Exclude specific device form factors from delivery (e.g., exclude CTV for app-install campaigns).',
            min_length=1,
        ),
    ] = None
    browser: Annotated[
        list[browser_family.BrowserFamily] | None,
        Field(
            description='Restrict delivery to specific canonical browser families in the impression delivery and rendering environment, not the post-click landing-page browser. Values MUST NOT be inferred solely from operating system, device, web/mobile-web inventory, or placement. Values in this array use OR semantics. When browser is supplied, families not listed are ineligible: other includes a seller-recognized family that is not explicitly enumerated, while unknown includes a browser the seller cannot classify into a recognized family. When the same family appears in browser and browser_exclude, exclusion wins. Browser and device constraints intersect; a seller that cannot enforce the exact combination MUST exclude or explicitly reconfigure the product during discovery and MUST reject it at create or update rather than silently widening delivery. Browser versions and seller-native IDs are intentionally unsupported.',
            min_length=1,
        ),
    ] = None
    browser_exclude: Annotated[
        list[browser_family.BrowserFamily] | None,
        Field(
            description='Exclude specific canonical browser families from delivery. other excludes seller-recognized families that are not explicitly enumerated; unknown excludes browsers the seller cannot classify into a recognized family. When the same family appears in browser and browser_exclude, exclusion wins. Sellers MUST reject a request they cannot enforce rather than silently dropping the exclusion.',
            min_length=1,
        ),
    ] = None
    store_catchments: Annotated[
        list[StoreCatchment] | None,
        Field(
            description='Target users within store catchment areas from a synced store catalog. Each entry references a store-type catalog and optionally narrows to specific stores or catchment zones.',
            min_length=1,
        ),
    ] = None
    geo_proximity: Annotated[
        list[GeoProximityItem] | None,
        Field(
            description='Target users within travel time, distance, or a custom boundary around arbitrary geographic points. Multiple entries use OR semantics — a user within range of any listed point is eligible. For campaigns targeting 10+ locations, consider using store_catchments with a location catalog instead. Seller must declare support in get_adcp_capabilities.',
            min_length=1,
        ),
    ] = None
    language: Annotated[
        list[locale_tag.LanguageTag] | None,
        Field(
            description="Restrict to users with specific language preferences using canonical BCP 47 language ranges. Each buyer range is evaluated against a user's language-preference tag with RFC 4647 section 3.3.1 Basic Filtering: 'fr' matches 'fr', 'fr-CA', and 'fr-FR', while 'fr-CA' matches 'fr-CA' and more-specific descendants but not 'fr' or 'fr-FR'. Values use OR logic.",
            min_length=1,
        ),
    ] = None
    keyword_targets: Annotated[
        list[KeywordTarget] | None,
        Field(
            description='Keyword targeting for search and retail media platforms. Restricts delivery to queries matching the specified keywords. Each keyword is identified by the tuple (keyword, match_type) — the same keyword string with different match types are distinct targets. Sellers SHOULD reject duplicate (keyword, match_type) pairs within a single request. Seller must declare support in get_adcp_capabilities.',
            min_length=1,
        ),
    ] = None
    negative_keywords: Annotated[
        list[NegativeKeyword] | None,
        Field(
            description='Keywords to exclude from delivery. Queries matching these keywords will not trigger the ad. Each negative keyword is identified by the tuple (keyword, match_type). Seller must declare support in get_adcp_capabilities.',
            min_length=1,
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var age_restriction : adcp.types.generated_poc.core.targeting.AgeRestriction | None
var audience_exclude : list[str] | None
var audience_include : list[str] | None
var axe_exclude_segment : str | None
var axe_include_segment : str | None
var browser : list[adcp.types.generated_poc.enums.browser_family.BrowserFamily] | None
var browser_exclude : list[adcp.types.generated_poc.enums.browser_family.BrowserFamily] | None
var collection_list : adcp.types.generated_poc.core.collection_list_ref.CollectionListReference | None
var collection_list_exclude : adcp.types.generated_poc.core.collection_list_ref.CollectionListReference | None
var daypart_targets : list[adcp.types.generated_poc.core.daypart_target.DaypartTarget] | None
var demographics : adcp.types.generated_poc.core.demographic_targeting_intent.DemographicTargetingIntent | None
var device_platform : list[adcp.types.generated_poc.enums.device_platform.DevicePlatform] | None
var device_platform_exclude : list[adcp.types.generated_poc.enums.device_platform.DevicePlatform] | None
var device_type : list[adcp.types.generated_poc.enums.device_type.DeviceType] | None
var device_type_exclude : list[adcp.types.generated_poc.enums.device_type.DeviceType] | None
var frequency_cap : adcp.types.generated_poc.core.frequency_cap.FrequencyCap | None
var geo_countries : list[adcp.types.generated_poc.core.targeting.GeoCountry] | None
var geo_countries_exclude : collections.abc.Sequence[adcp.types.generated_poc.core.targeting.GeoCountriesExcludeItem] | None
var geo_metros : list[adcp.types.generated_poc.core.targeting.GeoMetro] | None
var geo_metros_exclude : collections.abc.Sequence[adcp.types.generated_poc.core.targeting.GeoMetrosExcludeItem] | None
var geo_places : list[adcp.types.generated_poc.core.geo_place_area.GeographicPlaceArea] | None
var geo_places_exclude : list[adcp.types.generated_poc.core.geo_place_area.GeographicPlaceArea] | None
var geo_postal_areas : list[adcp.types.generated_poc.core.postal_area.PostalArea] | None
var geo_postal_areas_exclude : collections.abc.Sequence[adcp.types.generated_poc.core.postal_area.PostalArea] | None
var geo_proximity : list[adcp.types.generated_poc.core.targeting.GeoProximityItem] | None
var geo_regions : list[adcp.types.generated_poc.core.targeting.GeoRegion] | None
var geo_regions_exclude : collections.abc.Sequence[adcp.types.generated_poc.core.targeting.GeoRegionsExcludeItem] | None
var keyword_targets : list[adcp.types.generated_poc.core.targeting.KeywordTarget] | None
var language : list[adcp.types.generated_poc.core.locale_tag.LanguageTag] | None
var model_config
var negative_keywords : list[adcp.types.generated_poc.core.targeting.NegativeKeyword] | None
var placement_selection : adcp.types.generated_poc.core.placement_selection.PlacementSelection | None
var property_list : adcp.types.generated_poc.core.property_list_ref.PropertyListReference | None
var property_list_exclude : adcp.types.generated_poc.core.property_list_ref.PropertyListReference | None
var signal_targeting : list[adcp.types.generated_poc.core.signal_targeting.SignalTargeting] | None
var signal_targeting_groups : adcp.types.generated_poc.core.package_signal_targeting_groups.PackageSignalTargetingGroups | None
var store_catchments : list[adcp.types.generated_poc.core.targeting.StoreCatchment] | None

Inherited members

class TaskResult (**data: Any)
Expand source code
class TaskResult(BaseModel, Generic[T]):
    """Result from task execution."""

    model_config = ConfigDict(arbitrary_types_allowed=True)

    status: TaskStatus
    data: T | None = None
    message: str | None = None  # Human-readable message from agent (e.g., MCP content text)
    submitted: SubmittedInfo | None = None
    needs_input: NeedsInputInfo | None = None
    error: str | None = None
    # Structured AdCP error per transport-errors.mdx (``adcp_error`` object:
    # ``code``, ``message``, ``detail``, ``field_path``, ``recovery`` ...).
    # Always populated on the MCP FAILED path when the seller returned a
    # spec-shaped ``adcp_error`` — independent of ``debug``. Callers should
    # branch on ``adcp_error.code`` rather than regex-matching ``error``.
    adcp_error: dict[str, Any] | None = None
    success: bool = Field(default=True)
    metadata: dict[str, Any] | None = None
    debug_info: DebugInfo | None = None
    # The full idempotency_key the SDK used for this request — echoed here so
    # buyers can correlate against their own records. SENSITIVE inside the
    # seller's replay_ttl_seconds window (serves as a retry-pattern oracle);
    # do not emit to shared logs. The SDK's debug capture redacts keys by
    # default; avoid ``model_dump_json()``-ing a TaskResult into shared sinks.
    idempotency_key: str | None = None
    # True when the seller returned a cached response for a replayed key.
    # Agents that emit side effects on success (notifications, memory writes,
    # downstream tool calls) must check this flag and suppress duplicates.
    replayed: bool = False

Result from task execution.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • pydantic.main.BaseModel
  • typing.Generic

Subclasses

  • adcp.types.core.TaskResult[AdcpAsyncResponseData]
  • TaskResult[Any]
  • adcp.types.core.TaskResult[CheckGovernanceResponse]
  • adcp.types.core.TaskResult[ComplyTestControllerResponse]
  • adcp.types.core.TaskResult[ContextMatchResponseRouterPublisher]
  • adcp.types.core.TaskResult[CreateCollectionListResponse]
  • adcp.types.core.TaskResult[CreateContentStandardsResponse]
  • adcp.types.core.TaskResult[CreatePropertyListResponse]
  • adcp.types.core.TaskResult[DeleteCollectionListResponse]
  • adcp.types.core.TaskResult[DeletePropertyListResponse]
  • adcp.types.core.TaskResult[GetAdcpCapabilitiesResponse]
  • adcp.types.core.TaskResult[GetCollectionListResponse]
  • adcp.types.core.TaskResult[GetCreativeDeliveryResponse]
  • adcp.types.core.TaskResult[GetCreativeDeliveryResponse]
  • adcp.types.core.TaskResult[GetMediaBuyDeliveryResponse]
  • adcp.types.core.TaskResult[GetMediaBuyDeliveryResponse]
  • adcp.types.core.TaskResult[GetMediaBuysResponse]
  • adcp.types.core.TaskResult[GetMediaBuysResponse]
  • adcp.types.core.TaskResult[GetPlanAuditLogsResponse]
  • adcp.types.core.TaskResult[GetProductsResponse]
  • adcp.types.core.TaskResult[GetProductsResponse]
  • adcp.types.core.TaskResult[GetPropertyListResponse]
  • adcp.types.core.TaskResult[GetSignalsResponse]
  • adcp.types.core.TaskResult[GetTaskStatusResponse]
  • adcp.types.core.TaskResult[IdentityMatchResponseRouterPublisher]
  • adcp.types.core.TaskResult[ListAccountsResponse]
  • adcp.types.core.TaskResult[ListCollectionListsResponse]
  • adcp.types.core.TaskResult[ListContentStandardsResponse]
  • adcp.types.core.TaskResult[ListCreativeFormatsResponse]
  • adcp.types.core.TaskResult[ListCreativesResponse]
  • adcp.types.core.TaskResult[ListCreativesResponse]
  • adcp.types.core.TaskResult[ListPropertyListsResponse]
  • adcp.types.core.TaskResult[ListTasksResponse]
  • adcp.types.core.TaskResult[ListTransformersResponseCreativeAgent]
  • adcp.types.core.TaskResult[ReportPlanAdjustmentResponse]
  • adcp.types.core.TaskResult[ReportPlanOutcomeResponse]
  • adcp.types.core.TaskResult[ReportUsageResponse]
  • adcp.types.core.TaskResult[SiGetOfferingResponse]
  • adcp.types.core.TaskResult[SiInitiateSessionResponse]
  • adcp.types.core.TaskResult[SiSendMessageResponse]
  • adcp.types.core.TaskResult[SiTerminateSessionResponse]
  • adcp.types.core.TaskResult[SyncAgentNotificationConfigsResponse]
  • adcp.types.core.TaskResult[SyncGovernanceResponse]
  • adcp.types.core.TaskResult[SyncPlansResponse]
  • adcp.types.core.TaskResult[Union[AcceptProposalResponse5, AcceptProposalResponse6, AcceptProposalResponse7]]
  • adcp.types.core.TaskResult[Union[AcquireRightsResponse1, AcquireRightsResponse2, AcquireRightsResponse3, AcquireRightsResponse4]]
  • adcp.types.core.TaskResult[Union[ActivateSignalResponse1, ActivateSignalResponse2]]
  • adcp.types.core.TaskResult[Union[BuildCreativeResponse1, BuildCreativeResponse2, BuildCreativeResponse3, BuildCreativeResponse4, BuildCreativeResponse5, BuildCreativeResponse6]]
  • adcp.types.core.TaskResult[Union[BuyProductsResponse5, BuyProductsResponse6, BuyProductsResponse7]]
  • adcp.types.core.TaskResult[Union[CalibrateContentResponse1, CalibrateContentResponse2]]
  • adcp.types.core.TaskResult[Union[ControlMediaBuyResponse1, ControlMediaBuyResponse2, ControlMediaBuyResponse3]]
  • adcp.types.core.TaskResult[Union[CreateMediaBuyResponse1, CreateMediaBuyResponse2, CreateMediaBuyResponse3]]
  • adcp.types.core.TaskResult[Union[CreateMediaBuyResponse1, CreateMediaBuyResponse2, CreateMediaBuyResponse3]]
  • adcp.types.core.TaskResult[Union[DeclineProposalsResponse1, DeclineProposalsResponse2]]
  • adcp.types.core.TaskResult[Union[GetAccountFinancialsResponse1, GetAccountFinancialsResponse2]]
  • adcp.types.core.TaskResult[Union[GetBrandIdentityResponse1, GetBrandIdentityResponse2]]
  • adcp.types.core.TaskResult[Union[GetContentStandardsResponse1, GetContentStandardsResponse2]]
  • adcp.types.core.TaskResult[Union[GetCreativeFeaturesResponse1, GetCreativeFeaturesResponse2]]
  • adcp.types.core.TaskResult[Union[GetMediaBuyArtifactsResponse1, GetMediaBuyArtifactsResponse2]]
  • adcp.types.core.TaskResult[Union[GetRightsResponse1, GetRightsResponse2]]
  • adcp.types.core.TaskResult[Union[ListProductsResponse1, ListProductsResponse2]]
  • adcp.types.core.TaskResult[Union[LogEventResponse1, LogEventResponse2]]
  • adcp.types.core.TaskResult[Union[PreviewCreativeResponse1, PreviewCreativeResponse2, PreviewCreativeResponse3, PreviewCreativeResponse4]]
  • adcp.types.core.TaskResult[Union[ProvidePerformanceFeedbackResponse1, ProvidePerformanceFeedbackResponse2]]
  • adcp.types.core.TaskResult[Union[RefineProposalsResponse1, RefineProposalsResponse2]]
  • adcp.types.core.TaskResult[Union[RequestProposalsResponse1, RequestProposalsResponse2, RequestProposalsResponse3, RequestProposalsResponse4]]
  • adcp.types.core.TaskResult[Union[SyncAccountsResponse1, SyncAccountsResponse2]]
  • adcp.types.core.TaskResult[Union[SyncAudiencesResponse1, SyncAudiencesResponse2, SyncAudiencesResponse3]]
  • adcp.types.core.TaskResult[Union[SyncCatalogsResponse1, SyncCatalogsResponse2, SyncCatalogsResponse3]]
  • adcp.types.core.TaskResult[Union[SyncCreativesResponse1, SyncCreativesResponse2, SyncCreativesResponse3]]
  • adcp.types.core.TaskResult[Union[SyncEventSourcesResponse1, SyncEventSourcesResponse2]]
  • adcp.types.core.TaskResult[Union[UpdateMediaBuyResponse1, UpdateMediaBuyResponse2, UpdateMediaBuyResponse3]]
  • adcp.types.core.TaskResult[Union[UpdateMediaBuyResponse1, UpdateMediaBuyResponse2, UpdateMediaBuyResponse3]]
  • adcp.types.core.TaskResult[Union[UpdateRightsResponse1, UpdateRightsResponse2]]
  • adcp.types.core.TaskResult[Union[ValidateContentDeliveryResponse1, ValidateContentDeliveryResponse2]]
  • adcp.types.core.TaskResult[UpdateCollectionListResponse]
  • adcp.types.core.TaskResult[UpdateContentStandardsResponse]
  • adcp.types.core.TaskResult[UpdatePropertyListResponse]

Class variables

var adcp_error : dict[str, typing.Any] | None
var data : ~T | None
var debug_infoDebugInfo | None
var error : str | None
var idempotency_key : str | None
var message : str | None
var metadata : dict[str, typing.Any] | None
var model_config
var needs_inputNeedsInputInfo | None
var replayed : bool
var statusTaskStatus
var submittedSubmittedInfo | None
var success : bool
class TaskStatus (*args, **kwds)
Expand source code
class TaskStatus(str, Enum):
    """Task execution status."""

    COMPLETED = "completed"
    SUBMITTED = "submitted"
    NEEDS_INPUT = "needs_input"
    FAILED = "failed"
    WORKING = "working"

Task execution status.

Ancestors

  • builtins.str
  • enum.Enum

Class variables

var COMPLETED
var FAILED
var NEEDS_INPUT
var SUBMITTED
var WORKING
class GeneratedTaskStatus (*args, **kwds)
Expand source code
class TaskStatus(StrEnum):
    submitted = 'submitted'
    working = 'working'
    input_required = 'input-required'
    completed = 'completed'
    canceled = 'canceled'
    failed = 'failed'
    rejected = 'rejected'
    auth_required = 'auth-required'
    unknown = 'unknown'

Enum where members are also (and must be) strings

Ancestors

  • enum.StrEnum
  • builtins.str
  • enum.ReprEnum
  • enum.Enum

Class variables

var auth_required
var canceled
var completed
var failed
var input_required
var rejected
var submitted
var unknown
var working
class TextContent (**data: Any)
Expand source code
class TextAsset(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    asset_type: Annotated[
        Literal['text'],
        Field(
            description='Discriminator identifying this as a text asset. See /schemas/creative/asset-types for the registry.'
        ),
    ] = 'text'
    content: Annotated[str, Field(description='Text content')]
    language: Annotated[
        str | None,
        Field(
            description='Optional language claim for this text. In a materialized creative localization variant, localized-creative-asset.json requires this value to use /schemas/core/locale-tag.json and conformance requires exact equality with the enclosing variant locale. General non-localized assets retain the legacy unconstrained string for compatibility.'
        ),
    ] = None
    provenance: Annotated[
        provenance_1.Provenance | None,
        Field(
            description='Provenance metadata for this asset, overrides manifest-level provenance'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var asset_type : Literal['text']
var content : str
var language : str | None
var model_config
var provenance : adcp.types.generated_poc.core.provenance.Provenance | None

Inherited members

class TimeBasedPricingOption (**data: Any)
Expand source code
class TimeBasedPricingOption(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    pricing_option_id: Annotated[
        str, Field(description='Unique identifier for this pricing option within the product')
    ]
    pricing_model: Annotated[
        Literal['time'],
        Field(description='Cost per time unit - rate scales with campaign duration'),
    ] = 'time'
    currency: Annotated[
        str,
        Field(
            description='ISO 4217 currency code',
            examples=['USD', 'EUR', 'GBP', 'JPY'],
            pattern='^[A-Z]{3}$',
        ),
    ]
    fixed_price: Annotated[
        float | None,
        Field(
            description='Cost per time unit. If present, this is fixed pricing. If absent, auction-based.',
            ge=0.0,
        ),
    ] = None
    floor_price: Annotated[
        float | None,
        Field(
            description='Minimum acceptable bid per time unit for auction pricing (mutually exclusive with fixed_price). Bids below this value will be rejected.',
            ge=0.0,
        ),
    ] = None
    price_guidance: Annotated[
        price_guidance_1.PriceGuidance | None,
        Field(description='Optional pricing guidance for auction-based bidding'),
    ] = None
    parameters: Annotated[Parameters, Field(description='Time-based pricing parameters')]
    min_spend_per_package: Annotated[
        float | None,
        Field(
            description='Minimum spend requirement per package using this pricing option, in the specified currency',
            ge=0.0,
        ),
    ] = None
    price_breakdown: Annotated[
        price_breakdown_1.PriceBreakdown | None,
        Field(
            description='Breakdown of how fixed_price was derived from the list (rate card) price. Only meaningful when fixed_price is present.'
        ),
    ] = None
    eligible_adjustments: Annotated[
        list[adjustment_kind.PriceAdjustmentKind] | None,
        Field(
            description='Adjustment kinds applicable to this pricing option. Tells buyer agents which adjustments are available before negotiation. When absent, no adjustments are pre-declared — the buyer should check price_breakdown if present.'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var currency : str
var eligible_adjustments : list[adcp.types.generated_poc.enums.adjustment_kind.PriceAdjustmentKind] | None
var fixed_price : float | None
var floor_price : float | None
var min_spend_per_package : float | None
var model_config
var parameters : adcp.types.generated_poc.pricing_options.time_option.Parameters
var price_breakdown : adcp.types.generated_poc.pricing_options.price_breakdown.PriceBreakdown | None
var price_guidance : adcp.types.generated_poc.pricing_options.price_guidance.PriceGuidance | None
var pricing_model : Literal['time']
var pricing_option_id : str

Inherited members

class TimeUnit (*args, **kwds)
Expand source code
class TimeUnit(StrEnum):
    hour = 'hour'
    day = 'day'
    week = 'week'
    month = 'month'

Enum where members are also (and must be) strings

Ancestors

  • enum.StrEnum
  • builtins.str
  • enum.ReprEnum
  • enum.Enum

Class variables

var day
var hour
var month
var week
class IdentityMatchTmpxMacro (**data: Any)
Expand source code
class TmpxMacro(AdCPBaseModel):
    """Deprecated 3.1.8 TMPX macro/value compatibility model."""

    model_config = ConfigDict(
        extra='forbid',
    )
    name: Annotated[
        str,
        Field(max_length=64, min_length=1, pattern='^[A-Z][A-Z0-9_]*$'),
    ]
    value: Annotated[str, Field(max_length=1024, min_length=1)]

Deprecated 3.1.8 TMPX macro/value compatibility model.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var model_config
var name : str
var value : str
class ProviderRegistrationTmpxMacro (root: RootModelRootType = PydanticUndefined, **data)
Expand source code
class TmpxMacro(RootModel[str]):
    """Deprecated 3.1.8 registered macro-name compatibility model."""

    root: Annotated[str, Field(max_length=64, min_length=1, pattern='^[A-Z][A-Z0-9_]*$')]

Deprecated 3.1.8 registered macro-name compatibility model.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • pydantic.root_model.RootModel[str]
  • pydantic.root_model.RootModel
  • pydantic.main.BaseModel
  • typing.Generic

Class variables

var model_config
var root : str
class Transform (*args, **kwds)
Expand source code
class Transform(StrEnum):
    date = 'date'
    divide = 'divide'
    boolean = 'boolean'
    split = 'split'  # type: ignore[assignment]

Enum where members are also (and must be) strings

Ancestors

  • enum.StrEnum
  • builtins.str
  • enum.ReprEnum
  • enum.Enum

Class variables

var boolean
var date
var divide
var split
class TranslateUniversalMacrosResult (url: str,
dropped_params: list[str],
unmapped_macros: list[str],
dropped_consent_macros: list[str],
frozen_consent_macros: list[str],
suspect_native_values: list[str])
Expand source code
@dataclass(slots=True)
class TranslateUniversalMacrosResult:
    """Translated URL and deterministic diagnostics.

    ``dropped_params`` preserves query-parameter occurrence order and may
    contain duplicate keys. All macro diagnostic lists are deduplicated.
    URL-scoped diagnostics use first query occurrence order; mapping-scoped
    diagnostics preserve mapping iteration order.
    """

    url: str
    dropped_params: list[str]
    unmapped_macros: list[str]
    dropped_consent_macros: list[str]
    frozen_consent_macros: list[str]
    suspect_native_values: list[str]

Translated URL and deterministic diagnostics.

dropped_params preserves query-parameter occurrence order and may contain duplicate keys. All macro diagnostic lists are deduplicated. URL-scoped diagnostics use first query occurrence order; mapping-scoped diagnostics preserve mapping iteration order.

Instance variables

Expand source code
@dataclass(slots=True)
class TranslateUniversalMacrosResult:
    """Translated URL and deterministic diagnostics.

    ``dropped_params`` preserves query-parameter occurrence order and may
    contain duplicate keys. All macro diagnostic lists are deduplicated.
    URL-scoped diagnostics use first query occurrence order; mapping-scoped
    diagnostics preserve mapping iteration order.
    """

    url: str
    dropped_params: list[str]
    unmapped_macros: list[str]
    dropped_consent_macros: list[str]
    frozen_consent_macros: list[str]
    suspect_native_values: list[str]
var dropped_params : list[str]
Expand source code
@dataclass(slots=True)
class TranslateUniversalMacrosResult:
    """Translated URL and deterministic diagnostics.

    ``dropped_params`` preserves query-parameter occurrence order and may
    contain duplicate keys. All macro diagnostic lists are deduplicated.
    URL-scoped diagnostics use first query occurrence order; mapping-scoped
    diagnostics preserve mapping iteration order.
    """

    url: str
    dropped_params: list[str]
    unmapped_macros: list[str]
    dropped_consent_macros: list[str]
    frozen_consent_macros: list[str]
    suspect_native_values: list[str]
Expand source code
@dataclass(slots=True)
class TranslateUniversalMacrosResult:
    """Translated URL and deterministic diagnostics.

    ``dropped_params`` preserves query-parameter occurrence order and may
    contain duplicate keys. All macro diagnostic lists are deduplicated.
    URL-scoped diagnostics use first query occurrence order; mapping-scoped
    diagnostics preserve mapping iteration order.
    """

    url: str
    dropped_params: list[str]
    unmapped_macros: list[str]
    dropped_consent_macros: list[str]
    frozen_consent_macros: list[str]
    suspect_native_values: list[str]
var suspect_native_values : list[str]
Expand source code
@dataclass(slots=True)
class TranslateUniversalMacrosResult:
    """Translated URL and deterministic diagnostics.

    ``dropped_params`` preserves query-parameter occurrence order and may
    contain duplicate keys. All macro diagnostic lists are deduplicated.
    URL-scoped diagnostics use first query occurrence order; mapping-scoped
    diagnostics preserve mapping iteration order.
    """

    url: str
    dropped_params: list[str]
    unmapped_macros: list[str]
    dropped_consent_macros: list[str]
    frozen_consent_macros: list[str]
    suspect_native_values: list[str]
var unmapped_macros : list[str]
Expand source code
@dataclass(slots=True)
class TranslateUniversalMacrosResult:
    """Translated URL and deterministic diagnostics.

    ``dropped_params`` preserves query-parameter occurrence order and may
    contain duplicate keys. All macro diagnostic lists are deduplicated.
    URL-scoped diagnostics use first query occurrence order; mapping-scoped
    diagnostics preserve mapping iteration order.
    """

    url: str
    dropped_params: list[str]
    unmapped_macros: list[str]
    dropped_consent_macros: list[str]
    frozen_consent_macros: list[str]
    suspect_native_values: list[str]
var url : str
Expand source code
@dataclass(slots=True)
class TranslateUniversalMacrosResult:
    """Translated URL and deterministic diagnostics.

    ``dropped_params`` preserves query-parameter occurrence order and may
    contain duplicate keys. All macro diagnostic lists are deduplicated.
    URL-scoped diagnostics use first query occurrence order; mapping-scoped
    diagnostics preserve mapping iteration order.
    """

    url: str
    dropped_params: list[str]
    unmapped_macros: list[str]
    dropped_consent_macros: list[str]
    frozen_consent_macros: list[str]
    suspect_native_values: list[str]
class UniversalMacroTranslationError (macro: str)
Expand source code
class UniversalMacroTranslationError(ValueError):
    """Typed rejection raised before an unsafe native token can be emitted."""

    code: UniversalMacroTranslationErrorCode
    macro: str

    def __init__(self, macro: str) -> None:
        self.code = "unsafe_native_mapping"
        self.macro = macro
        super().__init__(f"native mapping for {macro!r} contains an unsafe control character")

Typed rejection raised before an unsafe native token can be emitted.

Ancestors

  • builtins.ValueError
  • builtins.Exception
  • builtins.BaseException

Class variables

var code : Literal['unsafe_native_mapping']
var macro : str
class UnknownFieldPolicy (*args, **kwds)
Expand source code
class UnknownFieldPolicy(str, Enum):
    """Server-side policy for unknown top-level tool arguments.

    Runs at the transport boundary before Pydantic request-model coercion
    can silently accept or drop extra fields.
    """

    REJECT = "reject"
    STRIP = "strip"
    IGNORE = "ignore"

Server-side policy for unknown top-level tool arguments.

Runs at the transport boundary before Pydantic request-model coercion can silently accept or drop extra fields.

Ancestors

  • builtins.str
  • enum.Enum

Class variables

var IGNORE
var REJECT
var STRIP
class UpdateContentStandardsSuccessResponse (**data: Any)
Expand source code
class UpdateContentStandardsResponse(AdcpVersionEnvelope, ProtocolEnvelope):
    pass

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • adcp.types.generated_poc.core.protocol_envelope.ProtocolEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var model_config
class UpdateContentStandardsResponse1 (**data: Any)
Expand source code
class UpdateContentStandardsResponse(AdcpVersionEnvelope, ProtocolEnvelope):
    pass

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • adcp.types.generated_poc.core.protocol_envelope.ProtocolEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var model_config
class UpdateContentStandardsErrorResponse (**data: Any)
Expand source code
class UpdateContentStandardsResponse(AdcpVersionEnvelope, ProtocolEnvelope):
    pass

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • adcp.types.generated_poc.core.protocol_envelope.ProtocolEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var model_config

Inherited members

class UpdateFrequency (*args, **kwds)
Expand source code
class UpdateFrequency(StrEnum):
    realtime = 'realtime'
    hourly = 'hourly'
    daily = 'daily'
    weekly = 'weekly'

Enum where members are also (and must be) strings

Ancestors

  • enum.StrEnum
  • builtins.str
  • enum.ReprEnum
  • enum.Enum

Class variables

var daily
var hourly
var realtime
var weekly
class UpdateMediaBuyRequest (**data: Any)

Base class enforcing the primary canonical runtime boundary.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var account : adcp.types.generated_poc.core.account_ref.AccountReference
var adcp_major_version : int | None
var adcp_version : str | None
var bidding : adcp.types.generated_poc.core.bidding_policy.BiddingPolicy | None
var budget_allocation : adcp.types.generated_poc.core.budget_allocation.BudgetAllocation | None
var budget_cap_timezone : str | None
var canceled : Literal[True] | None
var cancellation_reason : str | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var daily_budget_cap : float | None
var end_time : pydantic.types.AwareDatetime | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var governance_context : str | None
var idempotency_key : str
var invoice_recipient : adcp.types.generated_poc.core.business_entity.BusinessEntity | None
var media_buy_id : str
var model_config
var name : str | None
var new_packages : list[PackageRequest] | None
var pacing : adcp.types.generated_poc.enums.pacing.Pacing | None
var packages : list[PackageUpdate] | None
var paused : bool | None
var push_notification_config : adcp.types.generated_poc.core.push_notification_config.PushNotificationConfig | None
var reporting_webhook : adcp.types.generated_poc.core.reporting_webhook.ReportingWebhook | None
var revision : int | None
var start_time : adcp.types.generated_poc.core.start_timing.StartTiming | None
var total_budget : adcp.types.generated_poc.media_buy.update_media_buy_request.TotalBudget | None
class LegacyUpdateMediaBuyRequest (**data: Any)
Expand source code
class UpdateMediaBuyRequest(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    governance_context: Annotated[
        str | None,
        Field(
            description='Opaque intent authorization for a commitment-increasing media-buy update.',
            max_length=4096,
            min_length=1,
            pattern='^[\\x20-\\x7E]+$',
        ),
    ] = None
    account: Annotated[
        account_ref.AccountReference,
        Field(
            description='Account that owns this media buy. Pass a natural key (brand, operator, optional sandbox) or a seller-assigned account_id from list_accounts. Required for governance checks and account resolution.'
        ),
    ]
    media_buy_id: Annotated[str, Field(description="Seller's ID of the media buy to update")]
    name: Annotated[
        str | None,
        Field(
            description='Replacement human-readable name for this media buy, used for trafficking UI display and operational communication. Sellers that cannot update name mid-flight SHOULD echo the prior unchanged value in the success response rather than silently dropping the field. This display label is not an identifier or financial reference.',
            max_length=255,
            min_length=1,
            pattern='\\S',
        ),
    ] = None
    revision: Annotated[
        int | None,
        Field(
            description="Expected current revision for optimistic concurrency. Optional for backward compatibility. When provided, sellers MUST reject the update with CONFLICT if the media buy's current revision does not match, and MUST enforce that comparison atomically with the write. Obtain from get_media_buys or the most recent create/update response.",
            ge=1,
        ),
    ] = None
    paused: Annotated[
        bool | None,
        Field(description='Pause/resume the entire media buy (true = paused, false = active)'),
    ] = None
    canceled: Annotated[
        Literal[True] | None,
        Field(
            description='Cancel the entire media buy. Cancellation is irreversible — canceled media buys cannot be reactivated. Sellers MAY reject with NOT_CANCELLABLE if the media buy cannot be canceled in its current state.'
        ),
    ] = None
    cancellation_reason: Annotated[
        str | None,
        Field(
            description='Reason for cancellation. Sellers SHOULD store this and return it in subsequent get_media_buys responses.',
            max_length=500,
        ),
    ] = None
    start_time: start_timing.StartTiming | None = None
    end_time: Annotated[
        AwareDatetime | None, Field(description='New end date/time in ISO 8601 format')
    ] = None
    total_budget: Annotated[
        TotalBudget | None,
        Field(
            description='Updated hard aggregate lifetime budget. currency MUST equal the existing media-buy currency; an update does not redenominate a buy. When supplied alone (without packages or new_packages), in fixed mode the seller MUST atomically scale every active package budget in proportion to its current committed budget, rejecting the entire request if any derived budget cannot be accepted. When supplied with packages or new_packages, the amount MUST equal the resulting fixed-mode package sum; the seller applies the explicit package mutations and rejects with VALIDATION_ERROR if the total is inconsistent. In seller-optimized mode this changes the shared pool without converting package caps into allocations. Already-spent amounts still count against the new total.'
        ),
    ] = None
    daily_budget_cap: Annotated[
        float | None,
        Field(
            description='Replace the hard aggregate daily cap; null removes it. Numeric changes apply immediately with current-day spend counted. A cap below that spend pauses delivery for the day. Package caps are unchanged. Requires advertised media_buy scope; otherwise rejected with UNSUPPORTED_FEATURE.',
            ge=0.0,
        ),
    ] = None
    budget_cap_timezone: Annotated[
        str | None,
        Field(
            description='Replace the shared IANA cap-day timezone; null restores the default selected by budget_capping.timezone_basis (Account.timezone or fixed_timezone). Requires buyer_timezone_override. Changes start at the next boundary in the previously effective timezone; numeric cap changes remain immediate.',
            min_length=1,
        ),
    ] = None
    budget_allocation: Annotated[
        budget_allocation_1.BudgetAllocation | None,
        Field(
            description='Updated allocation configuration. Switching between fixed and seller-optimized modes is allowed only when update_budget_allocation is advertised in available_actions and the resulting package constraints are valid.'
        ),
    ] = None
    pacing: Annotated[
        pacing_1.Pacing | None,
        Field(
            description='Updated aggregate media-buy pacing. Package pacing remains subordinate to this aggregate strategy.'
        ),
    ] = None
    bidding: Annotated[
        bidding_policy.BiddingPolicy | None,
        Field(
            description='Replace the complete media-buy-authored bidding default. An object replaces the prior block; `{automatic:true}` records an explicit automatic policy. null clears it; packages with explicit package.bidding remain explicit, while packages without overrides fall back to provider automatic delivery. Goal binding follows the resulting budget allocation: seller-optimized outcome controls bind to allocation goals, while fixed inherited cost_per requires compatible package result units. Monetary fields use the media-buy currency and all affected pricing options MUST match it. The seller MUST validate all resulting policies atomically before mutation.'
        ),
    ] = None
    packages: Annotated[
        Sequence[package_update.PackageUpdate] | None,
        Field(description='Package-specific updates for existing packages', min_length=1),
    ] = None
    invoice_recipient: Annotated[
        business_entity.BusinessEntity | None,
        Field(
            description="Update who receives the invoice for this buy. When provided, the seller invoices this entity instead of the account's default billing_entity. The seller MUST validate the invoice recipient is authorized for this account. When governance_agents are configured, the seller MUST include invoice_recipient in the check_governance request."
        ),
    ] = None
    new_packages: Annotated[
        list[package_request.PackageRequest] | None,
        Field(
            description='New packages to add to this media buy. Uses the same schema as create_media_buy packages. When budget_allocation is omitted or fixed, every new package MUST carry budget and MUST NOT carry min_spend_target. To add a package without a hard cap to an existing seller-optimized buy, include its resulting seller_optimized budget_allocation block in the update so the allocation context is schema-visible. Repeating an unchanged allocation block does not itself switch modes. Sellers that support mid-flight package additions advertise `add_packages` in both `valid_actions[]` (deprecated) and as an entry in `available_actions[]` (authoritative). Sellers that do not support this MUST reject with ACTION_NOT_ALLOWED (preferred) or UNSUPPORTED_FEATURE (legacy).',
            min_length=1,
        ),
    ] = None
    reporting_webhook: Annotated[
        reporting_webhook_1.ReportingWebhook | None,
        Field(
            description='Optional webhook configuration for automated reporting delivery. Updates the reporting configuration for this media buy.'
        ),
    ] = None
    push_notification_config: Annotated[
        push_notification_config_1.PushNotificationConfig | None,
        Field(
            description='Optional webhook configuration for async update notifications. Publisher will send webhook when update completes if operation takes longer than immediate response time. This is separate from reporting_webhook which configures ongoing campaign reporting.'
        ),
    ] = None
    idempotency_key: Annotated[
        str,
        Field(
            description='Client-generated idempotency key for safe retries. If an update fails without a response, resending with the same idempotency_key guarantees the update is applied at most once. MUST be unique per (seller, request) pair to prevent cross-seller correlation. Use a fresh UUID v4 for each request.',
            max_length=255,
            min_length=16,
            pattern='^[A-Za-z0-9_.:-]{16,255}$',
        ),
    ]
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var account : adcp.types.generated_poc.core.account_ref.AccountReference
var bidding : adcp.types.generated_poc.core.bidding_policy.BiddingPolicy | None
var budget_allocation : adcp.types.generated_poc.core.budget_allocation.BudgetAllocation | None
var budget_cap_timezone : str | None
var canceled : Literal[True] | None
var cancellation_reason : str | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var daily_budget_cap : float | None
var end_time : pydantic.types.AwareDatetime | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var governance_context : str | None
var idempotency_key : str
var invoice_recipient : adcp.types.generated_poc.core.business_entity.BusinessEntity | None
var media_buy_id : str
var model_config
var name : str | None
var new_packages : list[adcp.types.generated_poc.media_buy.package_request.PackageRequest] | None
var pacing : adcp.types.generated_poc.enums.pacing.Pacing | None
var packages : collections.abc.Sequence[adcp.types.generated_poc.media_buy.package_update.PackageUpdate] | None
var paused : bool | None
var push_notification_config : adcp.types.generated_poc.core.push_notification_config.PushNotificationConfig | None
var reporting_webhook : adcp.types.generated_poc.core.reporting_webhook.ReportingWebhook | None
var revision : int | None
var start_time : adcp.types.generated_poc.core.start_timing.StartTiming | None
var total_budget : adcp.types.generated_poc.media_buy.update_media_buy_request.TotalBudget | None
class UpdateMediaBuyPackagesRequest (**data: Any)

Base class enforcing the primary canonical runtime boundary.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var account : adcp.types.generated_poc.core.account_ref.AccountReference
var adcp_major_version : int | None
var adcp_version : str | None
var bidding : adcp.types.generated_poc.core.bidding_policy.BiddingPolicy | None
var budget_allocation : adcp.types.generated_poc.core.budget_allocation.BudgetAllocation | None
var budget_cap_timezone : str | None
var canceled : Literal[True] | None
var cancellation_reason : str | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var daily_budget_cap : float | None
var end_time : pydantic.types.AwareDatetime | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var governance_context : str | None
var idempotency_key : str
var invoice_recipient : adcp.types.generated_poc.core.business_entity.BusinessEntity | None
var media_buy_id : str
var model_config
var name : str | None
var new_packages : list[PackageRequest] | None
var pacing : adcp.types.generated_poc.enums.pacing.Pacing | None
var packages : list[PackageUpdate] | None
var paused : bool | None
var push_notification_config : adcp.types.generated_poc.core.push_notification_config.PushNotificationConfig | None
var reporting_webhook : adcp.types.generated_poc.core.reporting_webhook.ReportingWebhook | None
var revision : int | None
var start_time : adcp.types.generated_poc.core.start_timing.StartTiming | None
var total_budget : adcp.types.generated_poc.media_buy.update_media_buy_request.TotalBudget | None
class UpdateMediaBuyPropertiesRequest (**data: Any)

Base class enforcing the primary canonical runtime boundary.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var account : adcp.types.generated_poc.core.account_ref.AccountReference
var adcp_major_version : int | None
var adcp_version : str | None
var bidding : adcp.types.generated_poc.core.bidding_policy.BiddingPolicy | None
var budget_allocation : adcp.types.generated_poc.core.budget_allocation.BudgetAllocation | None
var budget_cap_timezone : str | None
var canceled : Literal[True] | None
var cancellation_reason : str | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var daily_budget_cap : float | None
var end_time : pydantic.types.AwareDatetime | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var governance_context : str | None
var idempotency_key : str
var invoice_recipient : adcp.types.generated_poc.core.business_entity.BusinessEntity | None
var media_buy_id : str
var model_config
var name : str | None
var new_packages : list[PackageRequest] | None
var pacing : adcp.types.generated_poc.enums.pacing.Pacing | None
var packages : list[PackageUpdate] | None
var paused : bool | None
var push_notification_config : adcp.types.generated_poc.core.push_notification_config.PushNotificationConfig | None
var reporting_webhook : adcp.types.generated_poc.core.reporting_webhook.ReportingWebhook | None
var revision : int | None
var start_time : adcp.types.generated_poc.core.start_timing.StartTiming | None
var total_budget : adcp.types.generated_poc.media_buy.update_media_buy_request.TotalBudget | None

Inherited members

class UpdateMediaBuySuccessResponse (**data: Any)
Expand source code
class UpdateMediaBuyResponse1(_UpdateMediaBuyResponse1Base):
    """Canonical update response preserving the 3.x legacy-status normalizer."""

    @model_validator(mode="before")
    @classmethod
    def _normalize_legacy_status(cls, data: Any) -> Any:
        if not isinstance(data, dict):
            return data
        raw_status = unwrap_enum_value(data.get("status"))
        media_buy_status = unwrap_enum_value(data.get("media_buy_status"))
        if raw_status is None or raw_status == "completed":
            return {**data, "status": "completed"}
        if media_buy_status is None and raw_status in MEDIA_BUY_LEGACY_STATUS_VALUES:
            return {**data, "media_buy_status": raw_status, "status": "completed"}
        if media_buy_status is not None and raw_status == media_buy_status:
            return {**data, "status": "completed"}
        return data

Canonical update response preserving the 3.x legacy-status normalizer.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var model_config
class UpdateMediaBuyResponse1 (**data: Any)
Expand source code
class UpdateMediaBuyResponse1(_UpdateMediaBuyResponse1Base):
    """Canonical update response preserving the 3.x legacy-status normalizer."""

    @model_validator(mode="before")
    @classmethod
    def _normalize_legacy_status(cls, data: Any) -> Any:
        if not isinstance(data, dict):
            return data
        raw_status = unwrap_enum_value(data.get("status"))
        media_buy_status = unwrap_enum_value(data.get("media_buy_status"))
        if raw_status is None or raw_status == "completed":
            return {**data, "status": "completed"}
        if media_buy_status is None and raw_status in MEDIA_BUY_LEGACY_STATUS_VALUES:
            return {**data, "media_buy_status": raw_status, "status": "completed"}
        if media_buy_status is not None and raw_status == media_buy_status:
            return {**data, "status": "completed"}
        return data

Canonical update response preserving the 3.x legacy-status normalizer.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var model_config

Inherited members

class UpdateMediaBuyErrorResponse (**data: Any)

Base class enforcing the primary canonical runtime boundary.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var adcp_major_version : int | None
var adcp_version : str | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var errors : list[adcp.types.generated_poc.core.error.Error]
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config

Inherited members

class UpdateMediaBuyResponse3 (**data: Any)

Base class enforcing the primary canonical runtime boundary.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var adcp_error : adcp.types.generated_poc.core.error.Error | None
var adcp_major_version : int | None
var adcp_version : str | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var context_id : str | None
var errors : list[adcp.types.generated_poc.core.error.Error] | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var governance_context : str | None
var message : str | None
var model_config
var payload : dict[str, typing.Any] | None
var push_notification_config : adcp.types.generated_poc.core.push_notification_config.PushNotificationConfig | None
var replayed : bool | None
var status : Literal[<TaskStatus.submitted: 'submitted'>]
var task_id : str
var timestamp : pydantic.types.AwareDatetime | None
class UpdateMediaBuySubmittedResponse (**data: Any)

Base class enforcing the primary canonical runtime boundary.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var adcp_error : adcp.types.generated_poc.core.error.Error | None
var adcp_major_version : int | None
var adcp_version : str | None
var context : adcp.types.generated_poc.core.context.ContextObject | None
var context_id : str | None
var errors : list[adcp.types.generated_poc.core.error.Error] | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var governance_context : str | None
var message : str | None
var model_config
var payload : dict[str, typing.Any] | None
var push_notification_config : adcp.types.generated_poc.core.push_notification_config.PushNotificationConfig | None
var replayed : bool | None
var status : Literal[<TaskStatus.submitted: 'submitted'>]
var task_id : str
var timestamp : pydantic.types.AwareDatetime | None

Inherited members

class UrlContent (**data: Any)
Expand source code
class UrlAsset(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    asset_type: Annotated[
        Literal['url'],
        Field(
            description='Discriminator identifying this as a URL asset. See /schemas/creative/asset-types for the registry.'
        ),
    ] = 'url'
    url: Annotated[
        str,
        Field(
            description='URL reference. May be a plain URI or an RFC 6570 URI template carrying AdCP universal macros (e.g., `{SKU}`, `{MEDIA_BUY_ID}`). Buyers MUST NOT pre-encode macro braces at sync time; the ad server URL-encodes substituted values at impression time. See docs/creative/universal-macros.mdx.'
        ),
    ]
    url_type: Annotated[
        url_asset_type.UrlAssetType | None,
        Field(
            description="Mechanism a receiver uses to invoke this URL (distinct from purpose, which lives in `url-asset-requirements.role`): `clickthrough` for user click destination (landing page), `tracker_pixel` for impression/event tracking via HTTP request (fires GET, expects pixel/204 response), `tracker_script` for measurement SDKs that must load as a <script> tag (OMID verification, native event trackers using method:2). SHOULD be present on every URL asset; senders that omit it force the receiver into the role-based fallback described in this schema's top-level description."
        ),
    ] = None
    description: Annotated[
        str | None, Field(description='Description of what this URL points to')
    ] = None
    provenance: Annotated[
        provenance_1.Provenance | None,
        Field(
            description='Provenance metadata for this asset, overrides manifest-level provenance'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var asset_type : Literal['url']
var description : str | None
var model_config
var provenance : adcp.types.generated_poc.core.provenance.Provenance | None
var url : str
var url_type : adcp.types.generated_poc.enums.url_asset_type.UrlAssetType | None

Inherited members

class ValidateContentDeliverySuccessResponse (**data: Any)
Expand source code
class ValidateContentDeliveryResponse1(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    summary: Summary
    results: list[Result]
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config
var results : list[adcp.types.generated_poc.content_standards.validate_content_delivery_response.Result]
var summary : adcp.types.generated_poc.content_standards.validate_content_delivery_response.Summary
class ValidateContentDeliveryResponse1 (**data: Any)
Expand source code
class ValidateContentDeliveryResponse1(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    summary: Summary
    results: list[Result]
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config
var results : list[adcp.types.generated_poc.content_standards.validate_content_delivery_response.Result]
var summary : adcp.types.generated_poc.content_standards.validate_content_delivery_response.Summary

Inherited members

class ValidateContentDeliveryErrorResponse (**data: Any)
Expand source code
class ValidateContentDeliveryResponse2(AdcpVersionEnvelope):
    model_config = ConfigDict(extra='allow')
    errors: list[error_1.Error]
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var errors : list[adcp.types.generated_poc.core.error.Error]
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config

Inherited members

class ValidateInputRequest (**data: Any)
Expand source code
class ValidateInputRequest(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    account: Annotated[
        account_ref.AccountReference | None,
        Field(
            description='Optional account scope for seller-specific product validation. Required by sellers that route product declarations by buyer account.'
        ),
    ] = None
    brand: Annotated[
        brand_ref.BrandReference | None,
        Field(
            description='Optional brand scope when account is omitted or the seller keys sandbox validation by brand identity.'
        ),
    ] = None
    manifest: Annotated[
        creative_manifest.CreativeManifest, Field(description='Creative manifest to validate.')
    ]
    targets: Annotated[
        list[Targets] | None,
        Field(
            description="Discriminated list of validation targets. Each entry mirrors the `target` shape on `validate-input-result.json` so the request/response wire shapes match exactly. Multi-target requests enable universal-creative scenarios where one manifest targets multiple sellers' format declarations in a single round-trip; the response carries one result per target in the same order.",
            max_length=50,
            min_length=1,
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var account : adcp.types.generated_poc.core.account_ref.AccountReference | None
var brand : adcp.types.generated_poc.core.brand_ref.BrandReference | None
var manifest : adcp.types.generated_poc.core.creative_manifest.CreativeManifest
var model_config
var targets : list[adcp.types.generated_poc.creative.validate_input_request.Targets] | None

Inherited members

class ValidateInputResponse (**data: Any)
Expand source code
class ValidateInputResponse(AdcpVersionEnvelope, ProtocolEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    results: Annotated[
        list[validate_input_result.ValidateInputResult],
        Field(description='Per-target validation results.'),
    ]

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • adcp.types.generated_poc.core.protocol_envelope.ProtocolEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var model_config
var results : list[adcp.types.generated_poc.creative.validate_input_result.ValidateInputResult]

Inherited members

class ValidationError (*args, **kwargs)
Expand source code
class ValidationError(ValueError):
    """Raised when runtime validation fails."""

    pass

Raised when runtime validation fails.

Ancestors

  • builtins.ValueError
  • builtins.Exception
  • builtins.BaseException
class ValidationHookConfig (requests: ValidationMode | None = None,
responses: ValidationMode | None = None,
unknown_fields: "UnknownFieldPolicy | Literal['reject', 'strip', 'ignore'] | None" = None)
Expand source code
@dataclass(frozen=True)
class ValidationHookConfig:
    """Per-side client validation modes.

    Defaults match the TS port (adcontextprotocol/adcp-client#694):

    * ``requests``: ``"warn"`` — strict would break callers that
      intentionally send partial payloads (error-path tests, exploratory
      probes). Storyboards and compliance runners that want hard-stop
      enforcement pass ``requests="strict"`` explicitly.
    * ``responses``: ``"strict"`` in dev/test, ``"warn"`` when
      ``ADCP_ENV`` is set to ``production`` / ``prod``. Strict-by-default
      makes the SDK a compliance harness: drift from an agent fails the
      task on the first call, not the Nth storyboard run.

    Resolution order for both sides at call time:

    1. Explicit value on this config (``requests=`` / ``responses=``).
    2. ``ADCP_VALIDATION_MODE`` env var (``strict`` / ``warn`` / ``off``)
       — applies to both sides unless overridden by an explicit value.
       Matches the TS port (adcontextprotocol/adcp-client).
    3. ``ADCP_ENV=prod|production`` flips the response default to
       ``warn``; requests fall back to the type default.
    4. Defaults: ``requests="warn"``, ``responses="strict"``.

    Only ``ADCP_ENV`` and ``ADCP_VALIDATION_MODE`` are consulted —
    generic ``ENV`` / ``ENVIRONMENT`` would collide with unrelated
    tooling (rails, postgres, 12-factor) and silently flip the SDK's
    default.
    """

    requests: ValidationMode | None = None
    responses: ValidationMode | None = None
    #: Server-side policy for unsupported top-level tool arguments.
    #: ``None`` preserves existing permissive behavior.
    unknown_fields: UnknownFieldPolicy | Literal["reject", "strip", "ignore"] | None = None

Per-side client validation modes.

Defaults match the TS port (adcontextprotocol/adcp-client#694):

  • requests: "warn" — strict would break callers that intentionally send partial payloads (error-path tests, exploratory probes). Storyboards and compliance runners that want hard-stop enforcement pass requests="strict" explicitly.
  • responses: "strict" in dev/test, "warn" when ADCP_ENV is set to production / prod. Strict-by-default makes the SDK a compliance harness: drift from an agent fails the task on the first call, not the Nth storyboard run.

Resolution order for both sides at call time:

  1. Explicit value on this config (requests= / responses=).
  2. ADCP_VALIDATION_MODE env var (strict / warn / off) — applies to both sides unless overridden by an explicit value. Matches the TS port (adcontextprotocol/adcp-client).
  3. ADCP_ENV=prod|production flips the response default to warn; requests fall back to the type default.
  4. Defaults: requests="warn", responses="strict".

Only ADCP_ENV and ADCP_VALIDATION_MODE are consulted — generic ENV / ENVIRONMENT would collide with unrelated tooling (rails, postgres, 12-factor) and silently flip the SDK's default.

Instance variables

var requests : Literal['strict', 'warn', 'off'] | None
var responses : Literal['strict', 'warn', 'off'] | None
var unknown_fieldsUnknownFieldPolicy | Literal['reject', 'strip', 'ignore'] | None

Server-side policy for unsupported top-level tool arguments. None preserves existing permissive behavior.

class ValidationIssue (pointer: str, message: str, keyword: str, schema_path: str, hint: str | None = None)
Expand source code
@dataclass(frozen=True)
class ValidationIssue:
    """A single validation failure.

    Attributes:
        pointer: RFC 6901 JSON Pointer to the offending field.
        message: Sanitized, value-free description of the failure.
            Safe to return over the wire; does not echo input data.
        keyword: jsonschema keyword that rejected the payload
            (``required``, ``type``, ``enum``, etc.).
        schema_path: Path inside the schema that rejected the payload.
        hint: Optional near-miss diagnostic naming the closest matching
            ``oneOf`` variant and the wrong discriminator key. Only
            populated when the heuristic in :mod:`adcp.validation.oneof_hints`
            picks a clear winner; ``None`` otherwise. Additive — clients
            that ignore the field behave as before.
    """

    pointer: str
    message: str
    keyword: str
    schema_path: str
    hint: str | None = None

A single validation failure.

Attributes
-----=
pointer
RFC 6901 JSON Pointer to the offending field.
message
Sanitized, value-free description of the failure. Safe to return over the wire; does not echo input data.
keyword
jsonschema keyword that rejected the payload (required, type, enum, etc.).
schema_path
Path inside the schema that rejected the payload.
hint
Optional near-miss diagnostic naming the closest matching oneOf variant and the wrong discriminator key. Only populated when the heuristic in :mod:adcp.validation.oneof_hints picks a clear winner; None otherwise. Additive — clients that ignore the field behave as before.

Instance variables

var hint : str | None
var keyword : str
var message : str
var pointer : str
var schema_path : str
class ValidationOutcome (valid: bool,
issues: list[ValidationIssue] = <factory>,
variant: str = 'skipped')
Expand source code
@dataclass(frozen=True)
class ValidationOutcome:
    valid: bool
    issues: list[ValidationIssue] = field(default_factory=list)
    variant: str = "skipped"

ValidationOutcome(valid: 'bool', issues: 'list[ValidationIssue]' = , variant: 'str' = 'skipped')

Instance variables

var issues : list[ValidationIssue]
var valid : bool
var variant : str
class ValidationResult (**data: Any)
Expand source code
class ValidationResult(RegistryBaseModel):
    valid: bool
    domain: str | None = None
    url: str | None = None
    errors: list[str | dict[str, Any]] | None = None
    warnings: list[str | dict[str, Any]] | None = None
    status_code: int | None = None
    raw_data: dict[str, Any] | None = None

Base model for registry API types.

Uses extra='allow' so that new fields from the registry API are preserved rather than dropped. This differs from AdCPBaseModel which defaults to extra='ignore' for protocol types.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var domain : str | None
var errors : list[str | dict[str, typing.Any]] | None
var model_config
var raw_data : dict[str, typing.Any] | None
var status_code : int | None
var url : str | None
var valid : bool
var warnings : list[str | dict[str, typing.Any]] | None
class ValueMacroMapping (value: str)
Expand source code
@dataclass(frozen=True, slots=True)
class ValueMacroMapping:
    """A literal value encoded with the RFC 3986 unreserved whitelist."""

    value: str

A literal value encoded with the RFC 3986 unreserved whitelist.

Instance variables

var value : str
Expand source code
@dataclass(frozen=True, slots=True)
class ValueMacroMapping:
    """A literal value encoded with the RFC 3986 unreserved whitelist."""

    value: str
class VastAsset (root: RootModelRootType = PydanticUndefined, **data)
Expand source code
class VastAsset(RootModel[VastAsset3 | VastAsset4]):
    root: Annotated[
        VastAsset3 | VastAsset4,
        Field(
            description='VAST (Video Ad Serving Template) tag for third-party video ad serving. Unlike the hosted `video` asset, a VAST tag carries no `width`/`height`: a VAST response can return multiple renditions of differing dimensions, and the player selects one per device at serve time, so there is no single width/height for the ad. Dimensional, duration, and codec *constraints* for a placement live on the format/requirements layer, not on this asset.',
            discriminator='delivery_type',
            title='VAST Asset',
        ),
    ]
    def __getattr__(self, name: str) -> Any:
        """Proxy attribute access to the wrapped type."""
        if name.startswith('_'):
            raise AttributeError(name)
        return getattr(self.root, name)

Usage Documentation

RootModel and Custom Root Types

A Pydantic BaseModel for the root object of the model.

Attributes
-----=
root
The root object of the model.
__pydantic_root_model__
Whether the model is a RootModel.
__pydantic_private__
Private fields in the model.
__pydantic_extra__
Extra fields in the model.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • pydantic.root_model.RootModel[Union[VastAsset3, VastAsset4]]
  • pydantic.root_model.RootModel
  • pydantic.main.BaseModel
  • typing.Generic

Class variables

var model_config
var root : adcp.types.generated_poc.core.assets.vast_asset.VastAsset3 | adcp.types.generated_poc.core.assets.vast_asset.VastAsset4
class UrlVastAsset (**data: Any)
Expand source code
class VastAsset1(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    asset_type: Annotated[
        Literal['vast'],
        Field(
            description='Discriminator identifying this as a VAST asset. See /schemas/creative/asset-types for the registry.'
        ),
    ] = 'vast'
    vast_version: Annotated[VastVersion | None, Field(description='VAST specification version')] = (
        None
    )
    vpaid_enabled: Annotated[
        bool | None,
        Field(description='Whether VPAID (Video Player-Ad Interface Definition) is supported'),
    ] = None
    duration_ms: Annotated[
        int | None, Field(description='Expected video duration in milliseconds (if known)', ge=0)
    ] = None
    tracking_events: Annotated[
        list[VastTrackingEvent] | None,
        Field(description='Tracking events supported by this VAST tag'),
    ] = None
    captions_url: Annotated[
        AnyUrl | None, Field(description='URL to captions file (WebVTT, SRT, etc.)')
    ] = None
    audio_description_url: Annotated[
        AnyUrl | None,
        Field(description='URL to audio description track for visually impaired users'),
    ] = None
    provenance: Annotated[
        Provenance | None,
        Field(
            description='Provenance metadata for this asset, overrides manifest-level provenance'
        ),
    ] = None
    delivery_type: Annotated[
        Literal['url'],
        Field(description='Discriminator indicating VAST is delivered via URL endpoint'),
    ] = 'url'
    url: Annotated[
        str,
        Field(
            description='URL endpoint that returns VAST XML. May carry unsubstituted ad-server macros — VAST-style `[MACRO]` and `${MACRO}` placeholders are accepted as-is (RFC 6570 syntax); buyers MUST NOT pre-encode macro delimiters, since players match the literal token at substitution time.'
        ),
    ]

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var asset_type : Literal['vast']
var audio_description_url : pydantic.networks.AnyUrl | None
var captions_url : pydantic.networks.AnyUrl | None
var delivery_type : Literal['url']
var duration_ms : int | None
var model_config
var provenance : adcp.types.generated_poc.core.assets.asset_union.Provenance | None
var tracking_events : list[adcp.types.generated_poc.core.assets.asset_union.VastTrackingEvent] | None
var url : str
var vast_version : adcp.types.generated_poc.core.assets.asset_union.VastVersion | None
var vpaid_enabled : bool | None

Inherited members

class InlineVastAsset (**data: Any)
Expand source code
class VastAsset2(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    asset_type: Annotated[
        Literal['vast'],
        Field(
            description='Discriminator identifying this as a VAST asset. See /schemas/creative/asset-types for the registry.'
        ),
    ] = 'vast'
    vast_version: Annotated[VastVersion | None, Field(description='VAST specification version')] = (
        None
    )
    vpaid_enabled: Annotated[
        bool | None,
        Field(description='Whether VPAID (Video Player-Ad Interface Definition) is supported'),
    ] = None
    duration_ms: Annotated[
        int | None, Field(description='Expected video duration in milliseconds (if known)', ge=0)
    ] = None
    tracking_events: Annotated[
        list[VastTrackingEvent] | None,
        Field(description='Tracking events supported by this VAST tag'),
    ] = None
    captions_url: Annotated[
        AnyUrl | None, Field(description='URL to captions file (WebVTT, SRT, etc.)')
    ] = None
    audio_description_url: Annotated[
        AnyUrl | None,
        Field(description='URL to audio description track for visually impaired users'),
    ] = None
    provenance: Annotated[
        Provenance | None,
        Field(
            description='Provenance metadata for this asset, overrides manifest-level provenance'
        ),
    ] = None
    delivery_type: Annotated[
        Literal['inline'],
        Field(description='Discriminator indicating VAST is delivered as inline XML content'),
    ] = 'inline'
    content: Annotated[str, Field(description='Inline VAST XML content')]

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var asset_type : Literal['vast']
var audio_description_url : pydantic.networks.AnyUrl | None
var captions_url : pydantic.networks.AnyUrl | None
var content : str
var delivery_type : Literal['inline']
var duration_ms : int | None
var model_config
var provenance : adcp.types.generated_poc.core.assets.asset_union.Provenance | None
var tracking_events : list[adcp.types.generated_poc.core.assets.asset_union.VastTrackingEvent] | None
var vast_version : adcp.types.generated_poc.core.assets.asset_union.VastVersion | None
var vpaid_enabled : bool | None

Inherited members

class VastTrackerAsset (**data: Any)
Expand source code
class VastTrackerAsset(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    asset_type: Annotated[
        Literal['vast_tracker'],
        Field(
            description='Discriminator identifying this as a VAST tracker asset. See /schemas/creative/asset-types for the registry.'
        ),
    ] = 'vast_tracker'
    vast_event: Annotated[
        vast_tracking_event.VastTrackingEvent,
        Field(
            description='The VAST tracking event this URL fires on. Maps 1:1 to the VAST `Tracking event="..."` attribute inside `TrackingEvents`. MUST NOT be `impression` (belongs in the VAST `Impression` element — model as a `url` asset with `url_type: "tracker_pixel"`), `clickTracking` / `customClick` (belong in `VideoClicks`), `error` (VAST `Error` element), or any of `viewable` / `notViewable` / `viewUndetermined` / `measurableImpression` / `viewableImpression` (children of the VAST `ViewableImpression` element, not `TrackingEvents`).'
        ),
    ]
    url: Annotated[
        str,
        Field(
            description='Tracker URL that fires when `vast_event` occurs. May carry AdCP universal macros (e.g., `{SKU}`, `{MEDIA_BUY_ID}`); the sales agent or ad server URL-encodes substituted values at serve time. See docs/creative/universal-macros.mdx.'
        ),
    ]
    offset: Annotated[
        str | None,
        Field(
            description='VAST `offset` attribute. Required when `vast_event` is `progress`; ignored otherwise. Format matches the VAST 4.2 XSD `Tracking@offset` pattern: `HH:MM:SS` or `HH:MM:SS.mmm` for absolute time (two-digit hours, minutes 00–59, seconds 00–59), or an integer percentage 0–100 suffixed with `%`. Negative offsets are NOT permitted — the VAST 4.2 XSD pattern does not allow a leading minus.',
            pattern='^(\\d{2}:[0-5]\\d:[0-5]\\d(\\.\\d{3})?|(100|\\d{1,2})%)$',
        ),
    ] = None
    target: Annotated[
        Target | None,
        Field(
            description='Which VAST creative element this tracker scopes to — `linear` for `<Linear>/<TrackingEvents>`, `non_linear` for `<NonLinearAds>/<TrackingEvents>`, `companion` for `<CompanionAds>/<Companion>/<TrackingEvents>`. VAST 4.2 places these under separate XML elements with separate event semantics (e.g., `acceptInvitation` is meaningful on non-linear / companion; `closeLinear` only on linear). Defaults to `linear`. Sales agents use this to place the tracker in the correct location during VAST assembly.'
        ),
    ] = Target.linear
    provenance: Annotated[
        provenance_1.Provenance | None,
        Field(
            description='Provenance metadata for this asset, overrides manifest-level provenance.'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var asset_type : Literal['vast_tracker']
var model_config
var offset : str | None
var provenance : adcp.types.generated_poc.core.provenance.Provenance | None
var target : adcp.types.generated_poc.core.assets.vast_tracker_asset.Target | None
var url : str
var vast_event : adcp.types.generated_poc.enums.vast_tracking_event.VastTrackingEvent

Inherited members

class VcpmPricingOption (**data: Any)
Expand source code
class VcpmPricingOption(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    pricing_option_id: Annotated[
        str, Field(description='Unique identifier for this pricing option within the product')
    ]
    pricing_model: Annotated[
        Literal['vcpm'], Field(description='Cost per 1,000 viewable impressions (MRC standard)')
    ] = 'vcpm'
    currency: Annotated[
        str,
        Field(
            description='ISO 4217 currency code',
            examples=['USD', 'EUR', 'GBP', 'JPY'],
            pattern='^[A-Z]{3}$',
        ),
    ]
    fixed_price: Annotated[
        float | None,
        Field(
            description='Fixed price per unit. If present, this is fixed pricing. If absent, auction-based.',
            ge=0.0,
        ),
    ] = None
    floor_price: Annotated[
        float | None,
        Field(
            description='Minimum acceptable bid for auction pricing (mutually exclusive with fixed_price). Bids below this value will be rejected.',
            ge=0.0,
        ),
    ] = None
    max_bid: Annotated[
        bool | None,
        Field(
            deprecated=True,
            description='DEPRECATED in 3.2 and removed in the next major. Legacy hint used only to normalize package bid_price to bidding.max_bid (true) or bidding.bid_amount (false/absent). New buyers express intent directly in bidding.',
        ),
    ] = False
    price_guidance: Annotated[
        price_guidance_1.PriceGuidance | None,
        Field(description='Optional pricing guidance for auction-based bidding'),
    ] = None
    min_spend_per_package: Annotated[
        float | None,
        Field(
            description='Minimum spend requirement per package using this pricing option, in the specified currency',
            ge=0.0,
        ),
    ] = None
    price_breakdown: Annotated[
        price_breakdown_1.PriceBreakdown | None,
        Field(
            description='Breakdown of how fixed_price was derived from the list (rate card) price. Only meaningful when fixed_price is present.'
        ),
    ] = None
    eligible_adjustments: Annotated[
        list[adjustment_kind.PriceAdjustmentKind] | None,
        Field(
            description='Adjustment kinds applicable to this pricing option. Tells buyer agents which adjustments are available before negotiation. When absent, no adjustments are pre-declared — the buyer should check price_breakdown if present.'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var currency : str
var eligible_adjustments : list[adcp.types.generated_poc.enums.adjustment_kind.PriceAdjustmentKind] | None
var fixed_price : float | None
var floor_price : float | None
var max_bid : bool | None
var min_spend_per_package : float | None
var model_config
var price_breakdown : adcp.types.generated_poc.pricing_options.price_breakdown.PriceBreakdown | None
var price_guidance : adcp.types.generated_poc.pricing_options.price_guidance.PriceGuidance | None
var pricing_model : Literal['vcpm']
var pricing_option_id : str
class VcpmAuctionPricingOption (**data: Any)
Expand source code
class VcpmPricingOption(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    pricing_option_id: Annotated[
        str, Field(description='Unique identifier for this pricing option within the product')
    ]
    pricing_model: Annotated[
        Literal['vcpm'], Field(description='Cost per 1,000 viewable impressions (MRC standard)')
    ] = 'vcpm'
    currency: Annotated[
        str,
        Field(
            description='ISO 4217 currency code',
            examples=['USD', 'EUR', 'GBP', 'JPY'],
            pattern='^[A-Z]{3}$',
        ),
    ]
    fixed_price: Annotated[
        float | None,
        Field(
            description='Fixed price per unit. If present, this is fixed pricing. If absent, auction-based.',
            ge=0.0,
        ),
    ] = None
    floor_price: Annotated[
        float | None,
        Field(
            description='Minimum acceptable bid for auction pricing (mutually exclusive with fixed_price). Bids below this value will be rejected.',
            ge=0.0,
        ),
    ] = None
    max_bid: Annotated[
        bool | None,
        Field(
            deprecated=True,
            description='DEPRECATED in 3.2 and removed in the next major. Legacy hint used only to normalize package bid_price to bidding.max_bid (true) or bidding.bid_amount (false/absent). New buyers express intent directly in bidding.',
        ),
    ] = False
    price_guidance: Annotated[
        price_guidance_1.PriceGuidance | None,
        Field(description='Optional pricing guidance for auction-based bidding'),
    ] = None
    min_spend_per_package: Annotated[
        float | None,
        Field(
            description='Minimum spend requirement per package using this pricing option, in the specified currency',
            ge=0.0,
        ),
    ] = None
    price_breakdown: Annotated[
        price_breakdown_1.PriceBreakdown | None,
        Field(
            description='Breakdown of how fixed_price was derived from the list (rate card) price. Only meaningful when fixed_price is present.'
        ),
    ] = None
    eligible_adjustments: Annotated[
        list[adjustment_kind.PriceAdjustmentKind] | None,
        Field(
            description='Adjustment kinds applicable to this pricing option. Tells buyer agents which adjustments are available before negotiation. When absent, no adjustments are pre-declared — the buyer should check price_breakdown if present.'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var currency : str
var eligible_adjustments : list[adcp.types.generated_poc.enums.adjustment_kind.PriceAdjustmentKind] | None
var fixed_price : float | None
var floor_price : float | None
var max_bid : bool | None
var min_spend_per_package : float | None
var model_config
var price_breakdown : adcp.types.generated_poc.pricing_options.price_breakdown.PriceBreakdown | None
var price_guidance : adcp.types.generated_poc.pricing_options.price_guidance.PriceGuidance | None
var pricing_model : Literal['vcpm']
var pricing_option_id : str
class VcpmFixedRatePricingOption (**data: Any)
Expand source code
class VcpmPricingOption(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    pricing_option_id: Annotated[
        str, Field(description='Unique identifier for this pricing option within the product')
    ]
    pricing_model: Annotated[
        Literal['vcpm'], Field(description='Cost per 1,000 viewable impressions (MRC standard)')
    ] = 'vcpm'
    currency: Annotated[
        str,
        Field(
            description='ISO 4217 currency code',
            examples=['USD', 'EUR', 'GBP', 'JPY'],
            pattern='^[A-Z]{3}$',
        ),
    ]
    fixed_price: Annotated[
        float | None,
        Field(
            description='Fixed price per unit. If present, this is fixed pricing. If absent, auction-based.',
            ge=0.0,
        ),
    ] = None
    floor_price: Annotated[
        float | None,
        Field(
            description='Minimum acceptable bid for auction pricing (mutually exclusive with fixed_price). Bids below this value will be rejected.',
            ge=0.0,
        ),
    ] = None
    max_bid: Annotated[
        bool | None,
        Field(
            deprecated=True,
            description='DEPRECATED in 3.2 and removed in the next major. Legacy hint used only to normalize package bid_price to bidding.max_bid (true) or bidding.bid_amount (false/absent). New buyers express intent directly in bidding.',
        ),
    ] = False
    price_guidance: Annotated[
        price_guidance_1.PriceGuidance | None,
        Field(description='Optional pricing guidance for auction-based bidding'),
    ] = None
    min_spend_per_package: Annotated[
        float | None,
        Field(
            description='Minimum spend requirement per package using this pricing option, in the specified currency',
            ge=0.0,
        ),
    ] = None
    price_breakdown: Annotated[
        price_breakdown_1.PriceBreakdown | None,
        Field(
            description='Breakdown of how fixed_price was derived from the list (rate card) price. Only meaningful when fixed_price is present.'
        ),
    ] = None
    eligible_adjustments: Annotated[
        list[adjustment_kind.PriceAdjustmentKind] | None,
        Field(
            description='Adjustment kinds applicable to this pricing option. Tells buyer agents which adjustments are available before negotiation. When absent, no adjustments are pre-declared — the buyer should check price_breakdown if present.'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var currency : str
var eligible_adjustments : list[adcp.types.generated_poc.enums.adjustment_kind.PriceAdjustmentKind] | None
var fixed_price : float | None
var floor_price : float | None
var max_bid : bool | None
var min_spend_per_package : float | None
var model_config
var price_breakdown : adcp.types.generated_poc.pricing_options.price_breakdown.PriceBreakdown | None
var price_guidance : adcp.types.generated_poc.pricing_options.price_guidance.PriceGuidance | None
var pricing_model : Literal['vcpm']
var pricing_option_id : str

Inherited members

class VerifyBrandClaimPayload (**data: Any)
Expand source code
class VerifyBrandClaimPayload(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    typ: Annotated[
        Literal['adcp-response-payload+jws'],
        Field(description='Type discriminator preventing cross-profile replay.'),
    ]
    task: Annotated[
        Literal['verify_brand_claim'],
        Field(description='Designated task whose response payload is signed.'),
    ]
    brand_domain: Annotated[
        str,
        Field(
            description='Brand tenant whose policy store produced the answer. The signer MUST derive this from server-side tenant resolution, not caller-supplied request fields.',
            pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$',
        ),
    ]
    agent_url: Annotated[
        AnyUrl,
        Field(
            description='Canonical URL of the responding brand agent entry whose response-signing key verifies this envelope.'
        ),
    ]
    request_hash: Annotated[
        str,
        Field(
            description='sha256: prefix plus unpadded base64url SHA-256 of the canonical request-binding object for this call.',
            pattern='^sha256:[A-Za-z0-9_-]{43}$',
        ),
    ]
    iat: Annotated[int, Field(description='Issued-at time as Unix epoch seconds.', ge=0)]
    exp: Annotated[
        int,
        Field(
            description='Expiration time as Unix epoch seconds. Online verifiers reject envelopes after this time, allowing only implementation-defined clock skew.',
            ge=0,
        ),
    ]
    response: VerifyBrandClaimSignedSuccessPayload

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var agent_url : pydantic.networks.AnyUrl
var brand_domain : str
var exp : int
var iat : int
var model_config
var request_hash : str
var response : adcp.types.generated_poc.brand.verify_brand_claim_response.VerifyBrandClaimSignedSuccessPayload
var task : Literal['verify_brand_claim']
var typ : Literal['adcp-response-payload+jws']

Inherited members

class VerifyBrandClaimRequest (**data: Any)
Expand source code
class VerifyBrandClaimRequest(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    claim_type: Annotated[
        ClaimType,
        Field(description='Discriminates the kind of brand claim being verified.'),
    ]
    claim: Annotated[
        dict[str, Any],
        Field(description='Claim payload. Shape varies by claim_type.'),
    ]

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var claim : dict[str, typing.Any]
var claim_type : adcp.types.generated_poc.brand.verify_brand_claim_request.ClaimType
var model_config

Inherited members

class VerifyBrandClaimSignedResponse (**data: Any)
Expand source code
class VerifyBrandClaimSignedResponse(AdCPBaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    protected: Annotated[
        str,
        Field(
            description='Base64url-encoded JWS protected header. The decoded header MUST include alg, kid, and typ: adcp-response-payload+jws, and MUST NOT include the RFC 7797 b64 header. Verifiers enforce the key purpose by resolving kid to a JWK with adcp_use: response-signing.',
            pattern='^[A-Za-z0-9_-]+$',
        ),
    ]
    payload: Annotated[
        VerifyBrandClaimPayload,
        Field(
            description='Decoded signed payload. Signers compute the JWS payload bytes from the RFC 8785/JCS canonicalization of this object.'
        ),
    ]
    signature: Annotated[
        str,
        Field(
            description='Base64url-encoded JWS signature over the protected header and canonicalized payload.',
            pattern='^[A-Za-z0-9_-]+$',
        ),
    ]

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var model_config
var payload : adcp.types.generated_poc.brand.verify_brand_claim_response.VerifyBrandClaimPayload
var protected : str
var signature : str

Inherited members

class VerifyBrandClaimSignedSuccessPayload (**data: Any)
Expand source code
class VerifyBrandClaimSignedSuccessPayload(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    claim_type: ClaimType
    verification_status: verification_status.VerificationStatus
    details: dict[str, Any] | None = None
    context_note: Annotated[str | None, Field(max_length=500)] = None
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var claim_type : adcp.types.generated_poc.brand.verify_brand_claim_response.ClaimType
var context : adcp.types.generated_poc.core.context.ContextObject | None
var context_note : str | None
var details : dict[str, typing.Any] | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config
var verification_status : adcp.types.generated_poc.brand.verification_status.VerificationStatus

Inherited members

class VerifyBrandClaimsErrorResponse (**data: Any)
Expand source code
class VerifyBrandClaimsErrorResponse(AdcpVersionEnvelope, ProtocolEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    errors: Annotated[list[error_1.Error], Field(min_length=1)]
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • adcp.types.generated_poc.core.protocol_envelope.ProtocolEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var errors : list[adcp.types.generated_poc.core.error.Error]
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config

Inherited members

class VerifyBrandClaimsPayload (**data: Any)
Expand source code
class VerifyBrandClaimsPayload(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    typ: Annotated[
        Literal['adcp-response-payload+jws'],
        Field(description='Type discriminator preventing cross-profile replay.'),
    ]
    task: Annotated[
        Literal['verify_brand_claims'],
        Field(description='Designated task whose response payload is signed.'),
    ]
    brand_domain: Annotated[
        str,
        Field(
            description='Brand tenant whose policy store produced the answer. The signer MUST derive this from server-side tenant resolution, not caller-supplied request fields.',
            pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$',
        ),
    ]
    agent_url: Annotated[
        AnyUrl,
        Field(
            description='Canonical URL of the responding brand agent entry whose response-signing key verifies this envelope.'
        ),
    ]
    request_hash: Annotated[
        str,
        Field(
            description='sha256: prefix plus unpadded base64url SHA-256 of the canonical request-binding object for this call.',
            pattern='^sha256:[A-Za-z0-9_-]{43}$',
        ),
    ]
    iat: Annotated[int, Field(description='Issued-at time as Unix epoch seconds.', ge=0)]
    exp: Annotated[
        int,
        Field(
            description='Expiration time as Unix epoch seconds. Online verifiers reject envelopes after this time, allowing only implementation-defined clock skew.',
            ge=0,
        ),
    ]
    response: VerifyBrandClaimsSignedSuccessPayload

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var agent_url : pydantic.networks.AnyUrl
var brand_domain : str
var exp : int
var iat : int
var model_config
var request_hash : str
var response : adcp.types.generated_poc.brand.verify_brand_claims_response.VerifyBrandClaimsSignedSuccessPayload
var task : Literal['verify_brand_claims']
var typ : Literal['adcp-response-payload+jws']

Inherited members

class VerifyBrandClaimsRequest (**data: Any)
Expand source code
class VerifyBrandClaimsRequest(VerifyBrandClaimsRequestBulk):
    pass

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.brand.verify_brand_claims_request.VerifyBrandClaimsRequestBulk
  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var model_config

Inherited members

class VerifyBrandClaimsRequestBulk (**data: Any)
Expand source code
class VerifyBrandClaimsRequestBulk(AdcpVersionEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    claims: Annotated[
        list[ClaimEntry],
        Field(
            description='Ordered list of verification claims. The agent MUST return `results[]` in the same order (positional zip-by-index). Maximum batch size is 100 per call; agents MAY enforce a lower limit and SHOULD advertise it via `get_adcp_capabilities` (see the task page).',
            max_length=100,
            min_length=1,
        ),
    ]

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Subclasses

  • adcp.types.generated_poc.brand.verify_brand_claims_request.VerifyBrandClaimsRequest

Class variables

var claims : list[adcp.types.generated_poc.brand.verify_brand_claims_request.ClaimEntry]
var model_config

Inherited members

class VerifyBrandClaimsResponseBulk (**data: Any)
Expand source code
class VerifyBrandClaimsResponseBulk(AdcpVersionEnvelope, ProtocolEnvelope):
    model_config = ConfigDict(
        extra='allow',
    )
    results: Annotated[
        list[ResultEntry],
        Field(
            description="Per-claim results, positionally aligned with the request's claims.",
            min_length=1,
        ),
    ]
    signed_response: Annotated[
        VerifyBrandClaimsSignedResponse,
        Field(
            description='Payload-envelope JWS attesting the canonical bulk success response for verify_brand_claims. The signed payload response MUST match the unsigned task-body fields on this response, excluding signed_response and protocol/version envelope fields.'
        ),
    ]
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope
  • adcp.types.generated_poc.core.protocol_envelope.ProtocolEnvelope
  • AdCPBaseModel
  • pydantic.main.BaseModel

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config
var results : list[adcp.types.generated_poc.brand.verify_brand_claims_response.ResultEntry]
var signed_response : adcp.types.generated_poc.brand.verify_brand_claims_response.VerifyBrandClaimsSignedResponse

Inherited members

class VerifyBrandClaimsSignedResponse (**data: Any)
Expand source code
class VerifyBrandClaimsSignedResponse(AdCPBaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    protected: Annotated[
        str,
        Field(
            description='Base64url-encoded JWS protected header. The decoded header MUST include alg, kid, and typ: adcp-response-payload+jws, and MUST NOT include the RFC 7797 b64 header. Verifiers enforce the key purpose by resolving kid to a JWK with adcp_use: response-signing.',
            pattern='^[A-Za-z0-9_-]+$',
        ),
    ]
    payload: Annotated[
        VerifyBrandClaimsPayload,
        Field(
            description='Decoded signed payload. Signers compute the JWS payload bytes from the RFC 8785/JCS canonicalization of this object.'
        ),
    ]
    signature: Annotated[
        str,
        Field(
            description='Base64url-encoded JWS signature over the protected header and canonicalized payload.',
            pattern='^[A-Za-z0-9_-]+$',
        ),
    ]

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var model_config
var payload : adcp.types.generated_poc.brand.verify_brand_claims_response.VerifyBrandClaimsPayload
var protected : str
var signature : str

Inherited members

class VerifyBrandClaimsSignedSuccessPayload (**data: Any)
Expand source code
class VerifyBrandClaimsSignedSuccessPayload(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    results: Annotated[list[ResultEntry], Field(min_length=1)]
    context: context_1.ContextObject | None = None
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var context : adcp.types.generated_poc.core.context.ContextObject | None
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var model_config
var results : list[adcp.types.generated_poc.brand.verify_brand_claims_response.ResultEntry]

Inherited members

class VideoContent (**data: Any)
Expand source code
class VideoAsset(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    asset_type: Annotated[
        Literal['video'],
        Field(
            description='Discriminator identifying this as a video asset. See /schemas/creative/asset-types for the registry.'
        ),
    ] = 'video'
    url: Annotated[AnyUrl, Field(description='URL to the video asset')]
    width: Annotated[
        int,
        Field(
            description="Width in pixels — the video file's intrinsic native width. Required: a hosted file always has concrete dimensions. (Tag-delivered video carries no width; see the `vast` asset.)",
            ge=1,
        ),
    ]
    height: Annotated[
        int,
        Field(
            description="Height in pixels — the video file's intrinsic native height. Required: a hosted file always has concrete dimensions. (Tag-delivered video carries no height; see the `vast` asset.)",
            ge=1,
        ),
    ]
    duration_ms: Annotated[
        int | None, Field(description='Video duration in milliseconds', ge=1)
    ] = None
    file_size_bytes: Annotated[int | None, Field(description='File size in bytes', ge=1)] = None
    container_format: Annotated[
        str | None, Field(description='Video container format (mp4, webm, mov, etc.)')
    ] = None
    video_codec: Annotated[
        str | None, Field(description='Video codec used (h264, h265, vp9, av1, prores, etc.)')
    ] = None
    video_bitrate_kbps: Annotated[
        int | None, Field(description='Video stream bitrate in kilobits per second', ge=1)
    ] = None
    frame_rate: Annotated[
        str | None,
        Field(
            description="Frame rate as string to preserve precision (e.g., '23.976', '29.97', '30')"
        ),
    ] = None
    frame_rate_type: frame_rate_type_1.FrameRateType | None = None
    scan_type: scan_type_1.ScanType | None = None
    color_space: Annotated[ColorSpace | None, Field(description='Color space of the video')] = None
    hdr_format: Annotated[
        HdrFormat | None,
        Field(description="HDR format if applicable, or 'sdr' for standard dynamic range"),
    ] = None
    chroma_subsampling: Annotated[
        ChromaSubsampling | None, Field(description='Chroma subsampling format')
    ] = None
    video_bit_depth: Annotated[VideoBitDepth | None, Field(description='Video bit depth')] = None
    gop_interval_seconds: Annotated[
        float | None, Field(description='GOP/keyframe interval in seconds')
    ] = None
    gop_type: gop_type_1.GopType | None = None
    moov_atom_position: moov_atom_position_1.MoovAtomPosition | None = None
    has_audio: Annotated[
        bool | None, Field(description='Whether the video contains an audio track')
    ] = None
    audio_codec: Annotated[
        str | None,
        Field(description='Audio codec used (aac, aac_lc, he_aac, pcm, mp3, ac3, eac3, etc.)'),
    ] = None
    audio_sampling_rate_hz: Annotated[
        int | None, Field(description='Audio sampling rate in Hz (e.g., 44100, 48000)')
    ] = None
    audio_channels: Annotated[
        audio_channel_layout.AudioChannelLayout | None,
        Field(description='Audio channel configuration'),
    ] = None
    audio_bit_depth: Annotated[AudioBitDepth | None, Field(description='Audio bit depth')] = None
    audio_bitrate_kbps: Annotated[
        int | None, Field(description='Audio bitrate in kilobits per second', ge=1)
    ] = None
    audio_loudness_lufs: Annotated[
        float | None, Field(description='Integrated loudness in LUFS')
    ] = None
    audio_true_peak_dbfs: Annotated[float | None, Field(description='True peak level in dBFS')] = (
        None
    )
    captions_url: Annotated[
        AnyUrl | None, Field(description='URL to captions file (WebVTT, SRT, etc.)')
    ] = None
    transcript_url: Annotated[
        AnyUrl | None, Field(description='URL to text transcript of the video content')
    ] = None
    audio_description_url: Annotated[
        AnyUrl | None,
        Field(description='URL to audio description track for visually impaired users'),
    ] = None
    provenance: Annotated[
        provenance_1.Provenance | None,
        Field(
            description='Provenance metadata for this asset, overrides manifest-level provenance'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var asset_type : Literal['video']
var audio_bit_depth : adcp.types.generated_poc.core.assets.video_asset.AudioBitDepth | None
var audio_bitrate_kbps : int | None
var audio_channels : adcp.types.generated_poc.enums.audio_channel_layout.AudioChannelLayout | None
var audio_codec : str | None
var audio_description_url : pydantic.networks.AnyUrl | None
var audio_loudness_lufs : float | None
var audio_sampling_rate_hz : int | None
var audio_true_peak_dbfs : float | None
var captions_url : pydantic.networks.AnyUrl | None
var chroma_subsampling : adcp.types.generated_poc.core.assets.video_asset.ChromaSubsampling | None
var color_space : adcp.types.generated_poc.core.assets.video_asset.ColorSpace | None
var container_format : str | None
var duration_ms : int | None
var file_size_bytes : int | None
var frame_rate : str | None
var frame_rate_type : adcp.types.generated_poc.enums.frame_rate_type.FrameRateType | None
var gop_interval_seconds : float | None
var gop_type : adcp.types.generated_poc.enums.gop_type.GopType | None
var has_audio : bool | None
var hdr_format : adcp.types.generated_poc.core.assets.video_asset.HdrFormat | None
var height : int
var model_config
var moov_atom_position : adcp.types.generated_poc.enums.moov_atom_position.MoovAtomPosition | None
var provenance : adcp.types.generated_poc.core.provenance.Provenance | None
var scan_type : adcp.types.generated_poc.enums.scan_type.ScanType | None
var transcript_url : pydantic.networks.AnyUrl | None
var url : pydantic.networks.AnyUrl
var video_bit_depth : adcp.types.generated_poc.core.assets.video_asset.VideoBitDepth | None
var video_bitrate_kbps : int | None
var video_codec : str | None
var width : int

Inherited members

class WcagLevel (*args, **kwds)
Expand source code
class WcagLevel(StrEnum):
    A = 'A'
    AA = 'AA'
    AAA = 'AAA'

Enum where members are also (and must be) strings

Ancestors

  • enum.StrEnum
  • builtins.str
  • enum.ReprEnum
  • enum.Enum

Class variables

var A
var AA
var AAA
class WebhookContent (**data: Any)
Expand source code
class WebhookAsset(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    asset_type: Annotated[
        Literal['webhook'],
        Field(
            description='Discriminator identifying this as a webhook asset. See /schemas/creative/asset-types for the registry.'
        ),
    ] = 'webhook'
    url: Annotated[AnyUrl, Field(description='Webhook URL to call for dynamic content')]
    method: Annotated[http_method.HttpMethod | None, Field(description='HTTP method')] = (
        http_method.HttpMethod.POST
    )
    timeout_ms: Annotated[
        int | None,
        Field(description='Maximum time to wait for response in milliseconds', ge=10, le=5000),
    ] = 500
    supported_macros: Annotated[
        list[universal_macro.UniversalMacro | str] | None,
        Field(
            description='Universal macros that can be passed to webhook (e.g., DEVICE_TYPE, COUNTRY). See docs/creative/universal-macros.mdx for full list.'
        ),
    ] = None
    required_macros: Annotated[
        list[universal_macro.UniversalMacro | str] | None,
        Field(description='Universal macros that must be provided for webhook to function'),
    ] = None
    response_type: Annotated[
        webhook_response_type.WebhookResponseType,
        Field(description='Expected content type of webhook response'),
    ]
    security: Annotated[Security, Field(description='Security configuration for webhook calls')]
    provenance: Annotated[
        provenance_1.Provenance | None,
        Field(
            description='Provenance metadata for this asset, overrides manifest-level provenance'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var asset_type : Literal['webhook']
var method : adcp.types.generated_poc.enums.http_method.HttpMethod | None
var model_config
var provenance : adcp.types.generated_poc.core.provenance.Provenance | None
var required_macros : list[adcp.types.generated_poc.enums.universal_macro.UniversalMacro | str] | None
var response_type : adcp.types.generated_poc.enums.webhook_response_type.WebhookResponseType
var security : adcp.types.generated_poc.core.assets.webhook_asset.Security
var supported_macros : list[adcp.types.generated_poc.enums.universal_macro.UniversalMacro | str] | None
var timeout_ms : int | None
var url : pydantic.networks.AnyUrl

Inherited members

class WebhookChallenge (**data: Any)
Expand source code
class WebhookChallenge(AdCPBaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    type: Annotated[
        Literal['webhook.challenge'],
        Field(description='Discriminator for endpoint proof-of-control challenges.'),
    ] = 'webhook.challenge'
    challenge: Annotated[
        str,
        Field(
            description='Opaque, cryptographically random value that the receiver must echo in the response body. Recommended encoding: base64url without padding.',
            max_length=255,
            min_length=32,
            pattern='^[A-Za-z0-9_.:-]{32,255}$',
        ),
    ]
    account_id: Annotated[
        str,
        Field(
            description='Seller account identifier for the account whose notification_configs[] entry is being challenged.'
        ),
    ]
    subscriber_id: Annotated[
        str,
        Field(
            description='Buyer-supplied subscriber identifier from the notification_configs[] entry being challenged.',
            max_length=64,
            min_length=1,
            pattern='^[A-Za-z0-9_.:-]{1,64}$',
        ),
    ]
    seller_agent_url: Annotated[
        AnyUrl,
        Field(
            description='Exact seller agent URL whose RFC 9421 webhook profile key signs this challenge and that will send subsequent webhooks.'
        ),
    ]
    delivery_auth: Annotated[
        DeliveryAuth,
        Field(
            description='Authentication/signing mode the seller will use for subsequent webhooks delivered to this notification config.'
        ),
    ]
    event_types: Annotated[
        list[notification_type.NotificationType],
        Field(
            description='Normalized notification types requested by the subscriber at the time of the challenge. Part of the endpoint proof scope; changing event_types[] requires a fresh challenge before the new set can become active.',
            min_length=1,
        ),
    ]

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var account_id : str
var challenge : str
var delivery_auth : adcp.types.generated_poc.core.webhook_challenge.DeliveryAuth
var event_types : list[adcp.types.generated_poc.enums.notification_type.NotificationType]
var model_config
var seller_agent_url : pydantic.networks.AnyUrl
var subscriber_id : str
var type : Literal['webhook.challenge']

Inherited members

class WebhookChallengeError (message: str,
*,
reason: str,
field: str | None = None,
url: str | None = None,
status_code: int | None = None,
suggestion: str | None = None)
Expand source code
class WebhookChallengeError(ValueError):
    """Typed proof-of-control failure suitable for ``sync_accounts`` errors."""

    code = "INVALID_REQUEST"

    def __init__(
        self,
        message: str,
        *,
        reason: str,
        field: str | None = None,
        url: str | None = None,
        status_code: int | None = None,
        suggestion: str | None = None,
    ) -> None:
        super().__init__(message)
        self.reason = reason
        self.field = field
        self.url = url
        self.status_code = status_code
        self.suggestion = suggestion

    def to_error(self) -> dict[str, str]:
        """Return a small ``errors[]``-compatible dict for seller handlers."""

        error = {"code": self.code, "message": str(self)}
        if self.field is not None:
            error["field"] = self.field
        if self.suggestion is not None:
            error["suggestion"] = self.suggestion
        return error

Typed proof-of-control failure suitable for sync_accounts errors.

Ancestors

  • builtins.ValueError
  • builtins.Exception
  • builtins.BaseException

Class variables

var code

Methods

def to_error(self) ‑> dict[str, str]
Expand source code
def to_error(self) -> dict[str, str]:
    """Return a small ``errors[]``-compatible dict for seller handlers."""

    error = {"code": self.code, "message": str(self)}
    if self.field is not None:
        error["field"] = self.field
    if self.suggestion is not None:
        error["suggestion"] = self.suggestion
    return error

Return a small errors[]-compatible dict for seller handlers.

class WebhookChallengeResponse (**data: Any)
Expand source code
class WebhookChallengeResponse(AdCPBaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    challenge: Annotated[
        str | None,
        Field(
            description='Echo of the challenge value supplied by the seller.',
            max_length=255,
            min_length=32,
            pattern='^[A-Za-z0-9_.:-]{32,255}$',
        ),
    ] = None
    token: Annotated[
        str | None,
        Field(
            description='Backward-compatible alias for `challenge`. Receivers SHOULD prefer `challenge`; sellers MUST accept either field.',
            max_length=255,
            min_length=32,
            pattern='^[A-Za-z0-9_.:-]{32,255}$',
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var challenge : str | None
var model_config
var token : str | None

Inherited members

class WebhookChallengeResult (challenge: str,
echoed_field: str,
destination: WebhookDestinationValidation,
status_code: int,
response_headers: Mapping[str, str],
response_body: bytes)
Expand source code
@dataclass(frozen=True)
class WebhookChallengeResult:
    """Successful durable webhook proof-of-control challenge."""

    challenge: str
    echoed_field: str
    destination: WebhookDestinationValidation
    status_code: int
    response_headers: Mapping[str, str]
    response_body: bytes

    @property
    def ok(self) -> bool:
        return 200 <= self.status_code < 300

Successful durable webhook proof-of-control challenge.

Instance variables

var challenge : str
var destinationWebhookDestinationValidation
var echoed_field : str
prop ok : bool
Expand source code
@property
def ok(self) -> bool:
    return 200 <= self.status_code < 300
var response_body : bytes
var response_headers : Mapping[str, str]
var status_code : int
class WebhookDedupStore (backend: IdempotencyBackend,
ttl_seconds: int = 86400,
*,
processing_ttl_seconds: int | None = None,
namespace: str = 'webhook',
clock: Callable[[], float] = <built-in function time>)
Expand source code
class WebhookDedupStore:
    """Dedup ``(sender_id, idempotency_key)`` pairs to suppress retried webhooks.

    :param backend: any :class:`IdempotencyBackend`. Same MemoryBackend or
        PgBackend type used by :class:`IdempotencyStore` is fine — the
        ``namespace`` parameter prefixes all sender IDs so request-side and
        webhook-side scopes can't alias even when sharing one backend instance.
    :param ttl_seconds: payload-binding and replay window. Must be within ``[86400, 604800]`` per
        the spec minimum. Defaults to 86400 (24h).
    :param processing_ttl_seconds: processing-owner lease. An exact
        retry receives ``in_progress`` while the lease is live and may claim
        after it expires. Defaults to 300 seconds so a crashed handler cannot
        fence retries for the complete advertised delivery horizon. Configure
        it above the receiver's normal publication timeout; shorter leases
        increase the chance of overlapping owners and therefore require
        idempotent application publication. Must be positive and no longer
        than ``ttl_seconds``.
    :param namespace: prefix applied to every ``sender_id`` before it hits
        the backend. Defaults to ``"webhook"``, which is safe when the same
        backend is shared with :class:`IdempotencyStore` (request-side keys
        are scoped by a principal_id that isn't wrapped in this namespace,
        so collisions are impossible). Override only if you run multiple
        webhook scopes against one backend (e.g., separate dedup spaces for
        task webhooks vs list-change webhooks).
    :param clock: Deprecated compatibility argument. Expiry and lease decisions
        use :meth:`IdempotencyBackend.current_time` so durable backends and
        replicas share one authoritative clock.
    """

    def __init__(
        self,
        backend: IdempotencyBackend,
        ttl_seconds: int = _MIN_TTL_SECONDS,
        *,
        processing_ttl_seconds: int | None = None,
        namespace: str = "webhook",
        clock: Callable[[], float] = time.time,
    ) -> None:
        if not _MIN_TTL_SECONDS <= ttl_seconds <= _MAX_TTL_SECONDS:
            raise ValueError(
                f"ttl_seconds must be in [{_MIN_TTL_SECONDS}, {_MAX_TTL_SECONDS}] "
                f"per webhook spec minimum, got {ttl_seconds}"
            )
        if not namespace:
            raise ValueError("namespace must be a non-empty string")
        effective_processing_ttl = (
            _DEFAULT_PROCESSING_TTL_SECONDS
            if processing_ttl_seconds is None
            else processing_ttl_seconds
        )
        if not 1 <= effective_processing_ttl <= ttl_seconds:
            raise ValueError(
                "processing_ttl_seconds must be positive and no greater than "
                f"ttl_seconds, got {effective_processing_ttl}"
            )
        self.backend = backend
        self.ttl_seconds = ttl_seconds
        self.processing_ttl_seconds = effective_processing_ttl
        self.namespace = namespace
        _ = clock
        self._warned_legacy_backend = False

    @asynccontextmanager
    async def _hold(self, scope_key: str, key: str) -> AsyncIterator[None]:
        """Use the backend lock or a warned process-local compatibility lock."""
        if await self.backend.supports_atomic_hold():
            async with self.backend.hold(scope_key, key):
                yield
            return
        if not self._warned_legacy_backend:
            warnings.warn(
                f"{type(self.backend).__name__} does not implement hold(); using "
                "process-local webhook claim locking. This does not satisfy the "
                "multi-process AdCP 3.2 receiver contract; implement a durable "
                "atomic hold operation.",
                DeprecationWarning,
                stacklevel=3,
            )
            self._warned_legacy_backend = True

        slot = (scope_key, key)
        state = _legacy_backend_lock_state(self.backend)
        async with state.guard:
            lock = state.locks.get(slot)
            if lock is None:
                lock = asyncio.Lock()
                state.locks[slot] = lock
        async with lock:
            yield

    async def claim(
        self,
        sender_id: str,
        idempotency_key: str,
        payload_hash: str,
    ) -> WebhookDedupClaim:
        """Atomically claim a delivery or classify its retained state."""
        if not sender_id:
            raise ValueError("sender_id must be a non-empty string")
        if not idempotency_key:
            raise ValueError("idempotency_key must be a non-empty string")
        if not payload_hash:
            raise ValueError("payload_hash must be a non-empty string")

        scoped_sender = f"{self.namespace}:{sender_id}"
        async with self._hold(scoped_sender, idempotency_key):
            now = await self.backend.current_time()
            existing = await self.backend.get(scoped_sender, idempotency_key)
            if existing is not None:
                if existing.payload_hash != payload_hash:
                    return WebhookDedupClaim(status="conflict")
                state = existing.response.get("webhook_state")
                if state == "handled":
                    return WebhookDedupClaim(status="handled")
                lease_expires_at = existing.response.get("lease_expires_at")
                if (
                    state == "processing"
                    and isinstance(lease_expires_at, (int, float))
                    and lease_expires_at > now
                ):
                    return WebhookDedupClaim(status="in_progress")
                retain_until = existing.expires_at_epoch
            else:
                retain_until = now + self.ttl_seconds

            owner = secrets.token_urlsafe(32)
            claim_entry = CachedResponse(
                payload_hash=payload_hash,
                response={
                    "webhook_state": "processing",
                    "owner": owner,
                    "lease_expires_at": now + self.processing_ttl_seconds,
                },
                expires_at_epoch=retain_until,
            )
            if existing is None:
                await self.backend.put(scoped_sender, idempotency_key, claim_entry)
            else:
                await self.backend.replace(scoped_sender, idempotency_key, claim_entry)
            return WebhookDedupClaim(status="claimed", claim_token=owner)

    async def complete(
        self,
        sender_id: str,
        idempotency_key: str,
        payload_hash: str,
        claim_token: str,
    ) -> bool:
        """Durably mark an owned delivery handled.

        Returns ``True`` for the owner transition and ``False`` when the exact
        payload was already handled. Any missing, changed, or superseded claim
        fails closed with :class:`WebhookDedupOwnershipError`.
        """
        return await self._settle(
            sender_id,
            idempotency_key,
            payload_hash,
            claim_token,
            handled=True,
        )

    async def release(
        self,
        sender_id: str,
        idempotency_key: str,
        payload_hash: str,
        claim_token: str,
    ) -> bool:
        """Release an owned failed claim while retaining payload binding."""
        return await self._settle(
            sender_id,
            idempotency_key,
            payload_hash,
            claim_token,
            handled=False,
        )

    async def _settle(
        self,
        sender_id: str,
        idempotency_key: str,
        payload_hash: str,
        claim_token: str,
        *,
        handled: bool,
    ) -> bool:
        if not all((sender_id, idempotency_key, payload_hash, claim_token)):
            raise ValueError(
                "sender_id, idempotency_key, payload_hash, and claim_token are required"
            )
        scoped_sender = f"{self.namespace}:{sender_id}"
        async with self._hold(scoped_sender, idempotency_key):
            existing = await self.backend.get(scoped_sender, idempotency_key)
            if existing is None or existing.payload_hash != payload_hash:
                raise WebhookDedupOwnershipError("webhook delivery claim is missing or changed")
            state = existing.response.get("webhook_state")
            if handled and state == "handled":
                return False
            if state != "processing" or existing.response.get("owner") != claim_token:
                raise WebhookDedupOwnershipError("webhook delivery claim is not owned")
            response: dict[str, object]
            if handled:
                response = {"webhook_state": "handled"}
            else:
                response = {"webhook_state": "retryable"}
            await self.backend.replace(
                scoped_sender,
                idempotency_key,
                CachedResponse(
                    payload_hash=payload_hash,
                    response=response,
                    expires_at_epoch=existing.expires_at_epoch,
                ),
            )
            return True

Dedup (sender_id, idempotency_key) pairs to suppress retried webhooks.

:param backend: any :class:IdempotencyBackend. Same MemoryBackend or PgBackend type used by :class:IdempotencyStore is fine — the namespace parameter prefixes all sender IDs so request-side and webhook-side scopes can't alias even when sharing one backend instance. :param ttl_seconds: payload-binding and replay window. Must be within [86400, 604800] per the spec minimum. Defaults to 86400 (24h). :param processing_ttl_seconds: processing-owner lease. An exact retry receives in_progress while the lease is live and may claim after it expires. Defaults to 300 seconds so a crashed handler cannot fence retries for the complete advertised delivery horizon. Configure it above the receiver's normal publication timeout; shorter leases increase the chance of overlapping owners and therefore require idempotent application publication. Must be positive and no longer than ttl_seconds. :param namespace: prefix applied to every sender_id before it hits the backend. Defaults to "webhook", which is safe when the same backend is shared with :class:IdempotencyStore (request-side keys are scoped by a principal_id that isn't wrapped in this namespace, so collisions are impossible). Override only if you run multiple webhook scopes against one backend (e.g., separate dedup spaces for task webhooks vs list-change webhooks). :param clock: Deprecated compatibility argument. Expiry and lease decisions use :meth:IdempotencyBackend.current_time so durable backends and replicas share one authoritative clock.

Methods

async def claim(self, sender_id: str, idempotency_key: str, payload_hash: str) ‑> WebhookDedupClaim
Expand source code
async def claim(
    self,
    sender_id: str,
    idempotency_key: str,
    payload_hash: str,
) -> WebhookDedupClaim:
    """Atomically claim a delivery or classify its retained state."""
    if not sender_id:
        raise ValueError("sender_id must be a non-empty string")
    if not idempotency_key:
        raise ValueError("idempotency_key must be a non-empty string")
    if not payload_hash:
        raise ValueError("payload_hash must be a non-empty string")

    scoped_sender = f"{self.namespace}:{sender_id}"
    async with self._hold(scoped_sender, idempotency_key):
        now = await self.backend.current_time()
        existing = await self.backend.get(scoped_sender, idempotency_key)
        if existing is not None:
            if existing.payload_hash != payload_hash:
                return WebhookDedupClaim(status="conflict")
            state = existing.response.get("webhook_state")
            if state == "handled":
                return WebhookDedupClaim(status="handled")
            lease_expires_at = existing.response.get("lease_expires_at")
            if (
                state == "processing"
                and isinstance(lease_expires_at, (int, float))
                and lease_expires_at > now
            ):
                return WebhookDedupClaim(status="in_progress")
            retain_until = existing.expires_at_epoch
        else:
            retain_until = now + self.ttl_seconds

        owner = secrets.token_urlsafe(32)
        claim_entry = CachedResponse(
            payload_hash=payload_hash,
            response={
                "webhook_state": "processing",
                "owner": owner,
                "lease_expires_at": now + self.processing_ttl_seconds,
            },
            expires_at_epoch=retain_until,
        )
        if existing is None:
            await self.backend.put(scoped_sender, idempotency_key, claim_entry)
        else:
            await self.backend.replace(scoped_sender, idempotency_key, claim_entry)
        return WebhookDedupClaim(status="claimed", claim_token=owner)

Atomically claim a delivery or classify its retained state.

async def complete(self, sender_id: str, idempotency_key: str, payload_hash: str, claim_token: str) ‑> bool
Expand source code
async def complete(
    self,
    sender_id: str,
    idempotency_key: str,
    payload_hash: str,
    claim_token: str,
) -> bool:
    """Durably mark an owned delivery handled.

    Returns ``True`` for the owner transition and ``False`` when the exact
    payload was already handled. Any missing, changed, or superseded claim
    fails closed with :class:`WebhookDedupOwnershipError`.
    """
    return await self._settle(
        sender_id,
        idempotency_key,
        payload_hash,
        claim_token,
        handled=True,
    )

Durably mark an owned delivery handled.

Returns True for the owner transition and False when the exact payload was already handled. Any missing, changed, or superseded claim fails closed with :class:WebhookDedupOwnershipError.

async def release(self, sender_id: str, idempotency_key: str, payload_hash: str, claim_token: str) ‑> bool
Expand source code
async def release(
    self,
    sender_id: str,
    idempotency_key: str,
    payload_hash: str,
    claim_token: str,
) -> bool:
    """Release an owned failed claim while retaining payload binding."""
    return await self._settle(
        sender_id,
        idempotency_key,
        payload_hash,
        claim_token,
        handled=False,
    )

Release an owned failed claim while retaining payload binding.

class WebhookDestinationPolicy (require_https: bool = True,
allow_private_destinations: bool = False,
allowed_destination_ports: frozenset[int] | None = None,
transport_hooks: tuple[TransportHook, ...] = (),
name: str = 'production')
Expand source code
@dataclass(frozen=True)
class WebhookDestinationPolicy:
    """Registration-time policy for durable buyer webhook URLs.

    Use :meth:`production` before persisting buyer-provided
    ``push_notification_config.url`` or
    ``accounts[].notification_configs[].url``. Use
    :meth:`local_development` only for tests and local fixtures that need
    HTTP localhost or private-network endpoints.
    """

    require_https: bool = True
    allow_private_destinations: bool = False
    allowed_destination_ports: frozenset[int] | None = None
    transport_hooks: tuple[TransportHook, ...] = ()
    name: str = "production"

    @classmethod
    def production(
        cls,
        *,
        allowed_destination_ports: frozenset[int] | None = None,
        transport_hooks: tuple[TransportHook, ...] = (),
    ) -> WebhookDestinationPolicy:
        """Production webhook policy: HTTPS and public routable IPs only."""

        return cls(
            require_https=True,
            allow_private_destinations=False,
            allowed_destination_ports=allowed_destination_ports,
            transport_hooks=transport_hooks,
            name="production",
        )

    @classmethod
    def local_development(
        cls,
        *,
        allowed_destination_ports: frozenset[int] | None = None,
        transport_hooks: tuple[TransportHook, ...] = (),
    ) -> WebhookDestinationPolicy:
        """Explicit dev/test policy: allows HTTP and private destinations.

        Cloud metadata endpoints remain blocked by the shared SSRF
        validator even when private destinations are allowed.
        """

        return cls(
            require_https=False,
            allow_private_destinations=True,
            allowed_destination_ports=allowed_destination_ports,
            transport_hooks=transport_hooks,
            name="local_development",
        )

Registration-time policy for durable buyer webhook URLs.

Use :meth:production before persisting buyer-provided push_notification_config.url or accounts[].notification_configs[].url. Use :meth:local_development only for tests and local fixtures that need HTTP localhost or private-network endpoints.

Static methods

def local_development(*,
allowed_destination_ports: frozenset[int] | None = None,
transport_hooks: tuple[TransportHook, ...] = ()) ‑> WebhookDestinationPolicy

Explicit dev/test policy: allows HTTP and private destinations.

Cloud metadata endpoints remain blocked by the shared SSRF validator even when private destinations are allowed.

def production(*,
allowed_destination_ports: frozenset[int] | None = None,
transport_hooks: tuple[TransportHook, ...] = ()) ‑> WebhookDestinationPolicy

Production webhook policy: HTTPS and public routable IPs only.

Instance variables

var allow_private_destinations : bool
var allowed_destination_ports : frozenset[int] | None
var name : str
var require_https : bool
var transport_hooks : tuple[TransportHook, ...]
class WebhookMetadata (**data: Any)
Expand source code
class WebhookMetadata(BaseModel):
    """Metadata passed to webhook handlers."""

    operation_id: str
    agent_id: str
    task_type: str
    status: TaskStatus
    sequence_number: int | None = None
    notification_type: Literal["scheduled", "final", "delayed"] | None = None
    timestamp: str

Metadata passed to webhook handlers.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • pydantic.main.BaseModel

Class variables

var agent_id : str
var model_config
var notification_type : Literal['scheduled', 'final', 'delayed'] | None
var operation_id : str
var sequence_number : int | None
var statusTaskStatus
var task_type : str
var timestamp : str
class WebhookReceiver (config: WebhookReceiverConfig)
Expand source code
class WebhookReceiver:
    """Stateless webhook entry point, one instance per receiver configuration.

    Instance state (``config``) is read-only after construction. Per-request
    state lives in the :class:`WebhookOutcome` returned from :meth:`receive`.
    """

    def __init__(self, config: WebhookReceiverConfig) -> None:
        if not config.receiver_scope:
            raise ValueError("WebhookReceiverConfig.receiver_scope must be non-empty")
        self._config = config

    async def receive(
        self,
        *,
        method: str,
        url: str,
        headers: Mapping[str, str],
        body: bytes,
    ) -> WebhookOutcome:
        """Verify, dedupe, parse. Returns a :class:`WebhookOutcome`.

        Never raises for sender-caused cryptographic or protocol failures —
        returns an outcome with ``rejected=True`` and populated
        ``response_headers`` so the caller can convert to an HTTP response
        without try/except around every call. Operational failures inside
        the dedup backend or verify-options factory MAY still raise; wrap
        the call if you need to 5xx cleanly on internal errors.
        """
        if not _content_type_is_json(headers):
            return _reject("content_type_invalid", sender_identity=None)
        if len(body) > _MAX_WEBHOOK_BODY_BYTES:
            return _reject("body_too_large", sender_identity=None)

        signer, rejection = await self._verify(method=method, url=url, headers=headers, body=body)
        if rejection is not None:
            return rejection
        assert signer is not None  # verification succeeded

        sender_id = signer.as_sender_identity()
        publisher_scope = self._config.publisher_scope_for(signer)
        if not isinstance(publisher_scope, str) or not publisher_scope:
            raise ValueError("publisher_scope_for must return a non-empty string")
        dedup_scope = json.dumps(
            [self._config.receiver_scope, publisher_scope, "delivery"],
            ensure_ascii=True,
            separators=(",", ":"),
        )
        observation_scope = json.dumps(
            [self._config.receiver_scope, publisher_scope, "terminal-observation"],
            ensure_ascii=True,
            separators=(",", ":"),
        )

        try:
            payload_dict = json.loads(body, object_pairs_hook=_unique_json_object)
        except (json.JSONDecodeError, _DuplicateJsonKeyError, RecursionError):
            return _reject("body_invalid_json", sender_identity=sender_id)
        if not isinstance(payload_dict, dict):
            return _reject("body_invalid_json", sender_identity=sender_id)
        try:
            _validate_json_depth(payload_dict, max_depth=_MAX_JSON_DEPTH)
        except _JsonDepthError:
            return _reject("payload_not_i_json", sender_identity=sender_id)

        idempotency_key = payload_dict.get("idempotency_key")
        if not isinstance(idempotency_key, str) or not idempotency_key:
            # Spec 3.0-rc: idempotency_key is REQUIRED on every webhook payload.
            return _reject("idempotency_key_missing", sender_identity=sender_id)
        if not _IDEMPOTENCY_KEY_RE.match(idempotency_key):
            # Non-conformant format — charset or length out of bounds.
            return _reject("idempotency_key_invalid", sender_identity=sender_id)

        try:
            canonical_payload = rfc8785.dumps(payload_dict)
        except (TypeError, ValueError, RecursionError):
            return _reject("payload_not_i_json", sender_identity=sender_id)
        payload_hash = hashlib.sha256(canonical_payload).hexdigest()
        claim = await self._config.dedup.claim(
            sender_id=dedup_scope,
            idempotency_key=idempotency_key,
            payload_hash=payload_hash,
        )

        if claim.status == "conflict":
            return WebhookOutcome(
                rejected=True,
                rejection_reason="idempotency_conflict",
                sender_identity=sender_id,
                idempotency_key=idempotency_key,
                _payload_hash=payload_hash,
                _dedup_scope=dedup_scope,
            )

        parsed = self._parse(payload_dict)
        if parsed is None:
            if claim.status == "claimed":
                assert claim.claim_token is not None
                await self._config.dedup.complete(
                    dedup_scope,
                    idempotency_key,
                    payload_hash,
                    claim.claim_token,
                )
            return _reject("payload_invalid", sender_identity=sender_id)

        observation = _terminal_observation(payload_dict, self._config.kind)
        observation_claim = None
        if claim.status == "claimed" and observation is not None:
            observation_key, observation_hash = observation
            observation_claim = await self._config.dedup.claim(
                sender_id=observation_scope,
                idempotency_key=observation_key,
                payload_hash=observation_hash,
            )
            assert claim.claim_token is not None
            if observation_claim.status == "conflict":
                await self._config.dedup.release(
                    dedup_scope,
                    idempotency_key,
                    payload_hash,
                    claim.claim_token,
                )
                return WebhookOutcome(
                    rejected=True,
                    rejection_reason="idempotency_conflict",
                    sender_identity=sender_id,
                    payload=parsed,
                    idempotency_key=idempotency_key,
                    _payload_hash=payload_hash,
                    _dedup_scope=dedup_scope,
                )
            if observation_claim.status == "handled":
                await self._config.dedup.complete(
                    dedup_scope,
                    idempotency_key,
                    payload_hash,
                    claim.claim_token,
                )
                return WebhookOutcome(
                    sender_identity=sender_id,
                    payload=parsed,
                    duplicate=True,
                    idempotency_key=idempotency_key,
                    _payload_hash=payload_hash,
                    _dedup_scope=dedup_scope,
                )
            if observation_claim.status == "in_progress":
                await self._config.dedup.release(
                    dedup_scope,
                    idempotency_key,
                    payload_hash,
                    claim.claim_token,
                )
                return WebhookOutcome(
                    sender_identity=sender_id,
                    payload=parsed,
                    in_progress=True,
                    idempotency_key=idempotency_key,
                    _payload_hash=payload_hash,
                    _dedup_scope=dedup_scope,
                )

        return WebhookOutcome(
            sender_identity=sender_id,
            payload=parsed,
            duplicate=claim.status == "handled",
            in_progress=claim.status == "in_progress",
            idempotency_key=idempotency_key,
            _payload_hash=payload_hash,
            _claim_token=claim.claim_token,
            _dedup_scope=dedup_scope,
            _observation_scope=observation_scope if observation is not None else None,
            _observation_key=observation[0] if observation is not None else None,
            _observation_hash=observation[1] if observation is not None else None,
            _observation_token=(
                observation_claim.claim_token if observation_claim is not None else None
            ),
        )

    async def acknowledge(self, outcome: WebhookOutcome) -> WebhookOutcome:
        """Durably acknowledge a fresh claim after publication succeeds."""
        sender_id, key, payload_hash, token = self._owned_claim(outcome)
        if outcome._observation_token is not None:
            assert outcome._observation_scope is not None
            assert outcome._observation_key is not None
            assert outcome._observation_hash is not None
            await self._config.dedup.complete(
                outcome._observation_scope,
                outcome._observation_key,
                outcome._observation_hash,
                outcome._observation_token,
            )
        await self._config.dedup.complete(sender_id, key, payload_hash, token)
        return replace(
            outcome,
            handled=True,
            _claim_token=None,
            _observation_token=None,
        )

    async def release(self, outcome: WebhookOutcome) -> WebhookOutcome:
        """Release a failed publication while retaining immutable payload binding."""
        sender_id, key, payload_hash, token = self._owned_claim(outcome)
        if outcome._observation_token is not None:
            assert outcome._observation_scope is not None
            assert outcome._observation_key is not None
            assert outcome._observation_hash is not None
            await self._config.dedup.release(
                outcome._observation_scope,
                outcome._observation_key,
                outcome._observation_hash,
                outcome._observation_token,
            )
        await self._config.dedup.release(sender_id, key, payload_hash, token)
        return replace(outcome, _claim_token=None, _observation_token=None)

    async def receive_and_process(
        self,
        *,
        method: str,
        url: str,
        headers: Mapping[str, str],
        body: bytes,
        handler: Callable[[WebhookPayload], Awaitable[None] | None],
    ) -> WebhookOutcome:
        """Verify, claim, publish, and durably acknowledge one delivery.

        Existing duplicates, conflicts, and in-progress deliveries return
        without calling ``handler``. Handler failures release the owner lease
        for an exact retry and are re-raised so the HTTP framework can return
        a retryable 5xx.
        """
        outcome = await self.receive(method=method, url=url, headers=headers, body=body)
        if outcome.http_status is not None:
            return outcome
        assert outcome.payload is not None
        try:
            result = handler(outcome.payload)
            if inspect.isawaitable(result):
                await result
        except BaseException:
            await self.release(outcome)
            raise
        return await self.acknowledge(outcome)

    @staticmethod
    def _owned_claim(outcome: WebhookOutcome) -> tuple[str, str, str, str]:
        values = (
            outcome._dedup_scope,
            outcome.idempotency_key,
            outcome._payload_hash,
            outcome._claim_token,
        )
        if not all(isinstance(value, str) and value for value in values):
            raise ValueError("WebhookOutcome does not carry an owned delivery claim")
        return cast(tuple[str, str, str, str], values)

    def receive_sync(
        self,
        *,
        method: str,
        url: str,
        headers: Mapping[str, str],
        body: bytes,
    ) -> WebhookOutcome:
        """Synchronous wrapper around :meth:`receive` for WSGI-style frameworks.

        This is the low-level verification/claim surface. A fresh outcome
        still requires asynchronous ``acknowledge`` or ``release``. Sync-only
        applications should normally use :meth:`receive_and_process_sync`,
        which owns the complete claim lifecycle in one event loop.

            @app.post("/webhooks/adcp")
            def hook():
                outcome = receiver.receive_sync(
                    method=request.method,
                    url=request.url,
                    headers=dict(request.headers),
                    body=request.get_data(),
                )
                ...

        Raises :class:`RuntimeError` if invoked from a thread that already has
        a running event loop — the underlying verify / dedup path is async and
        cannot be driven from inside an active loop without blocking it. From
        async code, call :meth:`receive` directly.
        """
        try:
            asyncio.get_running_loop()
        except RuntimeError:
            # No running loop in this thread — safe to spin one up.
            return asyncio.run(self.receive(method=method, url=url, headers=headers, body=body))
        raise RuntimeError(
            "WebhookReceiver.receive_sync() cannot be called from a running "
            "event loop. Use `await receiver.receive(...)` instead."
        )

    def receive_and_process_sync(
        self,
        *,
        method: str,
        url: str,
        headers: Mapping[str, str],
        body: bytes,
        handler: Callable[[WebhookPayload], None],
    ) -> WebhookOutcome:
        """Sync entry point that publishes and settles a delivery atomically."""
        try:
            asyncio.get_running_loop()
        except RuntimeError:
            return asyncio.run(
                self.receive_and_process(
                    method=method,
                    url=url,
                    headers=headers,
                    body=body,
                    handler=handler,
                )
            )
        raise RuntimeError(
            "WebhookReceiver.receive_and_process_sync() cannot be called from a "
            "running event loop. Use `await receiver.receive_and_process(...)` instead."
        )

    async def _verify(
        self,
        *,
        method: str,
        url: str,
        headers: Mapping[str, str],
        body: bytes,
    ) -> tuple[VerifiedSignerLike | None, WebhookOutcome | None]:
        """Returns (signer, None) on success or (None, rejection_outcome)."""
        has_9421 = _has_9421_headers(headers)

        if has_9421:
            try:
                signer = verify_webhook_signature(
                    method=method,
                    url=url,
                    headers=headers,
                    body=body,
                    options=self._config.verify_options,
                )
                return signer, None
            except SignatureVerificationError as exc:
                # Downgrade defense: when 9421 IS present but fails, do NOT
                # consult HMAC fallback by default. A MITM that stripped a
                # valid 9421 signature and replaced it with a forged HMAC one
                # is exactly what the downgrade guard exists for.
                fallback = self._config.legacy_hmac
                allow_hmac = fallback is not None and not fallback.only_when_9421_absent
                if not allow_hmac:
                    return None, WebhookOutcome(
                        rejected=True,
                        rejection_reason="signature_invalid",
                        response_headers=_www_authenticate_header(exc.code),
                    )
                logger.warning(
                    "9421 webhook verify failed (%s); trying HMAC legacy because "
                    "legacy_hmac.only_when_9421_absent=False is set",
                    exc.code,
                )

        fallback = self._config.legacy_hmac
        if fallback is None:
            # No 9421 headers AND no HMAC fallback configured → spec says 9421
            # is baseline-required in 3.0, so this is non-conformant.
            return None, WebhookOutcome(
                rejected=True,
                rejection_reason="signature_missing",
                response_headers=_www_authenticate_header("webhook_signature_required"),
            )

        hmac_options = fallback.options_for(headers)
        if hmac_options is None:
            return None, WebhookOutcome(
                rejected=True,
                rejection_reason="signature_missing",
                response_headers=_www_authenticate_header("webhook_signature_required"),
            )
        try:
            legacy_signer = verify_webhook_hmac(headers=headers, body=body, options=hmac_options)
            return legacy_signer, None
        except LegacyWebhookHmacError:
            return None, WebhookOutcome(
                rejected=True,
                rejection_reason="signature_legacy_failed",
                response_headers=_www_authenticate_header("webhook_signature_invalid"),
            )

    def _parse(self, payload_dict: dict[str, Any]) -> WebhookPayload | None:
        model = _MODEL_BY_KIND[self._config.kind]
        try:
            return cast(WebhookPayload, model.model_validate(payload_dict))
        except ValidationError as exc:
            # Operators need the field-level reason to diagnose sender bugs.
            # The receiver still returns payload_invalid downstream; this is
            # just observability.
            logger.warning(
                "webhook payload failed %s validation: %s",
                self._config.kind,
                exc.errors(include_url=False),
            )
            return None

Stateless webhook entry point, one instance per receiver configuration.

Instance state (adcp.config) is read-only after construction. Per-request state lives in the :class:WebhookOutcome returned from :meth:receive.

Methods

async def acknowledge(self, outcome: WebhookOutcome) ‑> WebhookOutcome
Expand source code
async def acknowledge(self, outcome: WebhookOutcome) -> WebhookOutcome:
    """Durably acknowledge a fresh claim after publication succeeds."""
    sender_id, key, payload_hash, token = self._owned_claim(outcome)
    if outcome._observation_token is not None:
        assert outcome._observation_scope is not None
        assert outcome._observation_key is not None
        assert outcome._observation_hash is not None
        await self._config.dedup.complete(
            outcome._observation_scope,
            outcome._observation_key,
            outcome._observation_hash,
            outcome._observation_token,
        )
    await self._config.dedup.complete(sender_id, key, payload_hash, token)
    return replace(
        outcome,
        handled=True,
        _claim_token=None,
        _observation_token=None,
    )

Durably acknowledge a fresh claim after publication succeeds.

async def receive(self, *, method: str, url: str, headers: Mapping[str, str], body: bytes) ‑> WebhookOutcome
Expand source code
async def receive(
    self,
    *,
    method: str,
    url: str,
    headers: Mapping[str, str],
    body: bytes,
) -> WebhookOutcome:
    """Verify, dedupe, parse. Returns a :class:`WebhookOutcome`.

    Never raises for sender-caused cryptographic or protocol failures —
    returns an outcome with ``rejected=True`` and populated
    ``response_headers`` so the caller can convert to an HTTP response
    without try/except around every call. Operational failures inside
    the dedup backend or verify-options factory MAY still raise; wrap
    the call if you need to 5xx cleanly on internal errors.
    """
    if not _content_type_is_json(headers):
        return _reject("content_type_invalid", sender_identity=None)
    if len(body) > _MAX_WEBHOOK_BODY_BYTES:
        return _reject("body_too_large", sender_identity=None)

    signer, rejection = await self._verify(method=method, url=url, headers=headers, body=body)
    if rejection is not None:
        return rejection
    assert signer is not None  # verification succeeded

    sender_id = signer.as_sender_identity()
    publisher_scope = self._config.publisher_scope_for(signer)
    if not isinstance(publisher_scope, str) or not publisher_scope:
        raise ValueError("publisher_scope_for must return a non-empty string")
    dedup_scope = json.dumps(
        [self._config.receiver_scope, publisher_scope, "delivery"],
        ensure_ascii=True,
        separators=(",", ":"),
    )
    observation_scope = json.dumps(
        [self._config.receiver_scope, publisher_scope, "terminal-observation"],
        ensure_ascii=True,
        separators=(",", ":"),
    )

    try:
        payload_dict = json.loads(body, object_pairs_hook=_unique_json_object)
    except (json.JSONDecodeError, _DuplicateJsonKeyError, RecursionError):
        return _reject("body_invalid_json", sender_identity=sender_id)
    if not isinstance(payload_dict, dict):
        return _reject("body_invalid_json", sender_identity=sender_id)
    try:
        _validate_json_depth(payload_dict, max_depth=_MAX_JSON_DEPTH)
    except _JsonDepthError:
        return _reject("payload_not_i_json", sender_identity=sender_id)

    idempotency_key = payload_dict.get("idempotency_key")
    if not isinstance(idempotency_key, str) or not idempotency_key:
        # Spec 3.0-rc: idempotency_key is REQUIRED on every webhook payload.
        return _reject("idempotency_key_missing", sender_identity=sender_id)
    if not _IDEMPOTENCY_KEY_RE.match(idempotency_key):
        # Non-conformant format — charset or length out of bounds.
        return _reject("idempotency_key_invalid", sender_identity=sender_id)

    try:
        canonical_payload = rfc8785.dumps(payload_dict)
    except (TypeError, ValueError, RecursionError):
        return _reject("payload_not_i_json", sender_identity=sender_id)
    payload_hash = hashlib.sha256(canonical_payload).hexdigest()
    claim = await self._config.dedup.claim(
        sender_id=dedup_scope,
        idempotency_key=idempotency_key,
        payload_hash=payload_hash,
    )

    if claim.status == "conflict":
        return WebhookOutcome(
            rejected=True,
            rejection_reason="idempotency_conflict",
            sender_identity=sender_id,
            idempotency_key=idempotency_key,
            _payload_hash=payload_hash,
            _dedup_scope=dedup_scope,
        )

    parsed = self._parse(payload_dict)
    if parsed is None:
        if claim.status == "claimed":
            assert claim.claim_token is not None
            await self._config.dedup.complete(
                dedup_scope,
                idempotency_key,
                payload_hash,
                claim.claim_token,
            )
        return _reject("payload_invalid", sender_identity=sender_id)

    observation = _terminal_observation(payload_dict, self._config.kind)
    observation_claim = None
    if claim.status == "claimed" and observation is not None:
        observation_key, observation_hash = observation
        observation_claim = await self._config.dedup.claim(
            sender_id=observation_scope,
            idempotency_key=observation_key,
            payload_hash=observation_hash,
        )
        assert claim.claim_token is not None
        if observation_claim.status == "conflict":
            await self._config.dedup.release(
                dedup_scope,
                idempotency_key,
                payload_hash,
                claim.claim_token,
            )
            return WebhookOutcome(
                rejected=True,
                rejection_reason="idempotency_conflict",
                sender_identity=sender_id,
                payload=parsed,
                idempotency_key=idempotency_key,
                _payload_hash=payload_hash,
                _dedup_scope=dedup_scope,
            )
        if observation_claim.status == "handled":
            await self._config.dedup.complete(
                dedup_scope,
                idempotency_key,
                payload_hash,
                claim.claim_token,
            )
            return WebhookOutcome(
                sender_identity=sender_id,
                payload=parsed,
                duplicate=True,
                idempotency_key=idempotency_key,
                _payload_hash=payload_hash,
                _dedup_scope=dedup_scope,
            )
        if observation_claim.status == "in_progress":
            await self._config.dedup.release(
                dedup_scope,
                idempotency_key,
                payload_hash,
                claim.claim_token,
            )
            return WebhookOutcome(
                sender_identity=sender_id,
                payload=parsed,
                in_progress=True,
                idempotency_key=idempotency_key,
                _payload_hash=payload_hash,
                _dedup_scope=dedup_scope,
            )

    return WebhookOutcome(
        sender_identity=sender_id,
        payload=parsed,
        duplicate=claim.status == "handled",
        in_progress=claim.status == "in_progress",
        idempotency_key=idempotency_key,
        _payload_hash=payload_hash,
        _claim_token=claim.claim_token,
        _dedup_scope=dedup_scope,
        _observation_scope=observation_scope if observation is not None else None,
        _observation_key=observation[0] if observation is not None else None,
        _observation_hash=observation[1] if observation is not None else None,
        _observation_token=(
            observation_claim.claim_token if observation_claim is not None else None
        ),
    )

Verify, dedupe, parse. Returns a :class:WebhookOutcome.

Never raises for sender-caused cryptographic or protocol failures — returns an outcome with rejected=True and populated response_headers so the caller can convert to an HTTP response without try/except around every call. Operational failures inside the dedup backend or verify-options factory MAY still raise; wrap the call if you need to 5xx cleanly on internal errors.

async def receive_and_process(self,
*,
method: str,
url: str,
headers: Mapping[str, str],
body: bytes,
handler: Callable[[WebhookPayload], Awaitable[None] | None]) ‑> WebhookOutcome
Expand source code
async def receive_and_process(
    self,
    *,
    method: str,
    url: str,
    headers: Mapping[str, str],
    body: bytes,
    handler: Callable[[WebhookPayload], Awaitable[None] | None],
) -> WebhookOutcome:
    """Verify, claim, publish, and durably acknowledge one delivery.

    Existing duplicates, conflicts, and in-progress deliveries return
    without calling ``handler``. Handler failures release the owner lease
    for an exact retry and are re-raised so the HTTP framework can return
    a retryable 5xx.
    """
    outcome = await self.receive(method=method, url=url, headers=headers, body=body)
    if outcome.http_status is not None:
        return outcome
    assert outcome.payload is not None
    try:
        result = handler(outcome.payload)
        if inspect.isawaitable(result):
            await result
    except BaseException:
        await self.release(outcome)
        raise
    return await self.acknowledge(outcome)

Verify, claim, publish, and durably acknowledge one delivery.

Existing duplicates, conflicts, and in-progress deliveries return without calling handler. Handler failures release the owner lease for an exact retry and are re-raised so the HTTP framework can return a retryable 5xx.

def receive_and_process_sync(self,
*,
method: str,
url: str,
headers: Mapping[str, str],
body: bytes,
handler: Callable[[WebhookPayload], None]) ‑> WebhookOutcome
Expand source code
def receive_and_process_sync(
    self,
    *,
    method: str,
    url: str,
    headers: Mapping[str, str],
    body: bytes,
    handler: Callable[[WebhookPayload], None],
) -> WebhookOutcome:
    """Sync entry point that publishes and settles a delivery atomically."""
    try:
        asyncio.get_running_loop()
    except RuntimeError:
        return asyncio.run(
            self.receive_and_process(
                method=method,
                url=url,
                headers=headers,
                body=body,
                handler=handler,
            )
        )
    raise RuntimeError(
        "WebhookReceiver.receive_and_process_sync() cannot be called from a "
        "running event loop. Use `await receiver.receive_and_process(...)` instead."
    )

Sync entry point that publishes and settles a delivery atomically.

def receive_sync(self, *, method: str, url: str, headers: Mapping[str, str], body: bytes) ‑> WebhookOutcome
Expand source code
def receive_sync(
    self,
    *,
    method: str,
    url: str,
    headers: Mapping[str, str],
    body: bytes,
) -> WebhookOutcome:
    """Synchronous wrapper around :meth:`receive` for WSGI-style frameworks.

    This is the low-level verification/claim surface. A fresh outcome
    still requires asynchronous ``acknowledge`` or ``release``. Sync-only
    applications should normally use :meth:`receive_and_process_sync`,
    which owns the complete claim lifecycle in one event loop.

        @app.post("/webhooks/adcp")
        def hook():
            outcome = receiver.receive_sync(
                method=request.method,
                url=request.url,
                headers=dict(request.headers),
                body=request.get_data(),
            )
            ...

    Raises :class:`RuntimeError` if invoked from a thread that already has
    a running event loop — the underlying verify / dedup path is async and
    cannot be driven from inside an active loop without blocking it. From
    async code, call :meth:`receive` directly.
    """
    try:
        asyncio.get_running_loop()
    except RuntimeError:
        # No running loop in this thread — safe to spin one up.
        return asyncio.run(self.receive(method=method, url=url, headers=headers, body=body))
    raise RuntimeError(
        "WebhookReceiver.receive_sync() cannot be called from a running "
        "event loop. Use `await receiver.receive(...)` instead."
    )

Synchronous wrapper around :meth:receive for WSGI-style frameworks.

This is the low-level verification/claim surface. A fresh outcome still requires asynchronous acknowledge or release. Sync-only applications should normally use :meth:receive_and_process_sync, which owns the complete claim lifecycle in one event loop.

@app.post("/webhooks/adcp")
def hook():
    outcome = receiver.receive_sync(
        method=request.method,
        url=request.url,
        headers=dict(request.headers),
        body=request.get_data(),
    )
    ...

Raises :class:RuntimeError if invoked from a thread that already has a running event loop — the underlying verify / dedup path is async and cannot be driven from inside an active loop without blocking it. From async code, call :meth:receive directly.

async def release(self, outcome: WebhookOutcome) ‑> WebhookOutcome
Expand source code
async def release(self, outcome: WebhookOutcome) -> WebhookOutcome:
    """Release a failed publication while retaining immutable payload binding."""
    sender_id, key, payload_hash, token = self._owned_claim(outcome)
    if outcome._observation_token is not None:
        assert outcome._observation_scope is not None
        assert outcome._observation_key is not None
        assert outcome._observation_hash is not None
        await self._config.dedup.release(
            outcome._observation_scope,
            outcome._observation_key,
            outcome._observation_hash,
            outcome._observation_token,
        )
    await self._config.dedup.release(sender_id, key, payload_hash, token)
    return replace(outcome, _claim_token=None, _observation_token=None)

Release a failed publication while retaining immutable payload binding.

class WebhookReceiverConfig (verify_options: WebhookVerifyOptions,
dedup: WebhookDedupStore,
receiver_scope: str,
publisher_scope_for: Callable[[VerifiedSignerLike], str],
legacy_hmac: LegacyHmacFallback | None = None,
kind: WebhookKind = 'mcp')
Expand source code
@dataclass(frozen=True)
class WebhookReceiverConfig:
    """Configuration bundle.

    :param verify_options: verifier configuration (JWKS, replay store, etc.).
        A single instance is reused for every request — the verifier stamps
        ``now`` itself via ``verify_options.clock()``, so there's no need to
        refresh a time field per request.
    :param dedup: webhook-dedup store.
    :param receiver_scope: trusted tenant/subscription/endpoint scope for this
        receiver instance. Never derive it from the webhook payload.
    :param publisher_scope_for: maps the verified signing identity to a stable
        seller/publisher identity. The returned value must survive signing-key
        rotation; key IDs are authentication evidence, not publication scope.
    :param legacy_hmac: optional HMAC-SHA256 fallback for 3.x migration.
    :param kind: which webhook payload type to parse into. Default ``"mcp"``
        (the task-status webhook that dominates most integrations); pass
        explicitly for list-change / artifact / revocation receivers.
    """

    verify_options: WebhookVerifyOptions
    dedup: WebhookDedupStore
    receiver_scope: str
    publisher_scope_for: Callable[[VerifiedSignerLike], str]
    legacy_hmac: LegacyHmacFallback | None = None
    kind: WebhookKind = "mcp"

Configuration bundle.

:param verify_options: verifier configuration (JWKS, replay store, etc.). A single instance is reused for every request — the verifier stamps now itself via verify_options.clock(), so there's no need to refresh a time field per request. :param dedup: webhook-dedup store. :param receiver_scope: trusted tenant/subscription/endpoint scope for this receiver instance. Never derive it from the webhook payload. :param publisher_scope_for: maps the verified signing identity to a stable seller/publisher identity. The returned value must survive signing-key rotation; key IDs are authentication evidence, not publication scope. :param legacy_hmac: optional HMAC-SHA256 fallback for 3.x migration. :param kind: which webhook payload type to parse into. Default "mcp" (the task-status webhook that dominates most integrations); pass explicitly for list-change / artifact / revocation receivers.

Instance variables

var dedupWebhookDedupStore
var kind : Literal['mcp', 'revocation_notification', 'collection_list_changed', 'property_list_changed', 'artifact']
var legacy_hmacLegacyHmacFallback | None
var publisher_scope_for : Callable[[VerifiedSignerLike], str]
var receiver_scope : str
var verify_optionsWebhookVerifyOptions
class WebhookSender (*,
private_key: PrivateKey,
key_id: str,
alg: str,
client: httpx.AsyncClient | None = None,
timeout_seconds: float = 10.0,
allow_private_destinations: bool = False,
allowed_destination_ports: frozenset[int] | None = None,
transport_hooks: tuple[TransportHook, ...] = ())
Expand source code
class WebhookSender:
    """Outbound signed-webhook delivery client.

    Owns one webhook-signing private key. Reuses a single :class:`httpx.AsyncClient`
    across requests for connection pooling — pass your own via ``client=`` if
    you want to share it with other SDK surfaces.

    Thread/task safety: safe to call concurrent ``send_*`` from many asyncio
    tasks. The underlying ``httpx.AsyncClient`` manages its own pool.
    """

    def __init__(
        self,
        *,
        private_key: PrivateKey,
        key_id: str,
        alg: str,
        client: httpx.AsyncClient | None = None,
        timeout_seconds: float = _DEFAULT_TIMEOUT_SECONDS,
        allow_private_destinations: bool = False,
        allowed_destination_ports: frozenset[int] | None = None,
        transport_hooks: tuple[TransportHook, ...] = (),
    ) -> None:
        """Construct a sender wired to RFC 9421 JWK signing.

        The HMAC and bearer modes are reached via :meth:`from_bearer_token`,
        :meth:`from_adcp_legacy_hmac`, and :meth:`from_standard_webhooks_secret`
        — those classmethods bypass this initializer through
        :meth:`_from_strategy` because their key material has different
        types (``bytes`` / ``str`` rather than ``PrivateKey``).

        ``transport_hooks`` runs URL rewrites before SSRF validation —
        see :class:`adcp.webhook_transport_hooks.DockerLocalhostRewrite`
        for the canonical use case. SSRF remains authoritative on the
        rewritten URL; hooks cannot punch through the range check.
        """
        self._auth: WebhookAuthStrategy = JwkSignerStrategy(
            private_key=private_key, key_id=key_id, alg=alg
        )
        self._key_id = key_id
        self._timeout = timeout_seconds
        self._client = client
        self._owns_client = client is None
        self._allow_private_destinations = allow_private_destinations
        self._allowed_destination_ports = allowed_destination_ports
        self._transport_hooks = tuple(transport_hooks)
        _validate_hooks(self._transport_hooks, allow_private_destinations)

    @classmethod
    def _from_strategy(
        cls,
        auth: WebhookAuthStrategy,
        *,
        key_id: str,
        client: httpx.AsyncClient | None,
        timeout_seconds: float,
        allow_private_destinations: bool,
        allowed_destination_ports: frozenset[int] | None,
        transport_hooks: tuple[TransportHook, ...] = (),
    ) -> WebhookSender:
        """Build a sender around a pre-constructed auth strategy.

        Internal constructor for the HMAC/bearer paths. The public
        ``__init__`` is locked to the JWK signature for back-compat;
        new modes don't fit that signature, so they bypass it here.
        """
        sender = cls.__new__(cls)
        sender._auth = auth
        sender._key_id = key_id
        sender._timeout = timeout_seconds
        sender._client = client
        sender._owns_client = client is None
        sender._allow_private_destinations = allow_private_destinations
        sender._allowed_destination_ports = allowed_destination_ports
        sender._transport_hooks = tuple(transport_hooks)
        _validate_hooks(sender._transport_hooks, allow_private_destinations)
        return sender

    @classmethod
    def from_jwk(
        cls,
        jwk: Mapping[str, Any],
        *,
        d_field: str = "d",
        client: httpx.AsyncClient | None = None,
        timeout_seconds: float = _DEFAULT_TIMEOUT_SECONDS,
        allow_private_destinations: bool = False,
        allowed_destination_ports: frozenset[int] | None = None,
        transport_hooks: tuple[TransportHook, ...] = (),
    ) -> WebhookSender:
        """Construct from a JWK that includes the private scalar.

        The JWK MUST have ``adcp_use == "webhook-signing"`` — the sender
        doesn't validate this (you're signing with your own key; validation
        happens at the receiver), but a key whose adcp_use is wrong will be
        rejected by every conformant verifier.

        ``allow_private_destinations`` and ``allowed_destination_ports``
        forward to :meth:`__init__` — see that signature for semantics.
        """
        # Snapshot the mapping once — a live Mapping could otherwise return
        # different values across the adcp_use / kid / d / alg reads.
        jwk_snapshot = dict(jwk)
        if jwk_snapshot.get("adcp_use") != "webhook-signing":
            raise ValueError(
                f"WebhookSender requires a JWK with adcp_use='webhook-signing' "
                f"(got {jwk_snapshot.get('adcp_use')!r}). Webhook-signing and "
                f"request-signing keys MUST be distinct so a signature from one "
                f"surface cannot be replayed as the other. Generate a separate "
                f"key with adcp_use='webhook-signing' and publish it in your "
                f"adagents.json alongside your request-signing key. See "
                f"https://adcontextprotocol.org/docs/building/implementation/security"
            )
        alg = jwk_snapshot.get("alg")
        if alg == "EdDSA":
            alg = "ed25519"
        elif alg == "ES256":
            alg = "ecdsa-p256-sha256"
        if alg not in ("ed25519", "ecdsa-p256-sha256"):
            raise ValueError(f"unsupported JWK alg {jwk_snapshot.get('alg')!r}")
        private_key = private_key_from_jwk(jwk_snapshot, d_field=d_field)
        return cls(
            private_key=private_key,
            key_id=str(jwk_snapshot["kid"]),
            alg=alg,
            client=client,
            timeout_seconds=timeout_seconds,
            allow_private_destinations=allow_private_destinations,
            allowed_destination_ports=allowed_destination_ports,
            transport_hooks=transport_hooks,
        )

    @classmethod
    def from_pem(
        cls,
        pem_path: str | Path | bytes,
        *,
        key_id: str,
        alg: str = "ed25519",
        passphrase: bytes | None = None,
        client: httpx.AsyncClient | None = None,
        timeout_seconds: float = _DEFAULT_TIMEOUT_SECONDS,
        allow_private_destinations: bool = False,
        allowed_destination_ports: frozenset[int] | None = None,
        transport_hooks: tuple[TransportHook, ...] = (),
    ) -> WebhookSender:
        """Load a private key from a PEM file and bind it as a webhook sender.

        Companion to ``adcp-keygen --purpose webhook-signing``, which writes
        the PEM and prints the public JWK. The JWK is published at your
        ``jwks_uri``; the PEM holds the private key material. ``from_pem``
        reads the PEM, constructs the right ``PrivateKey`` type for ``alg``,
        and returns a sender ready to send.

        Args:
            pem_path: Path to the PKCS#8 PEM, or the PEM bytes directly.
            key_id: JWK ``kid`` claim — must match the published JWK.
            alg: Signature algorithm. ``ed25519`` (default) or ``es256``.
                Also accepts the RFC 9421 form ``ecdsa-p256-sha256``.
            passphrase: Required if the PEM is encrypted
                (``adcp-keygen --encrypt``).
            client: Optional pre-built :class:`httpx.AsyncClient` to share
                across the SDK; the sender owns its own client when omitted.
            timeout_seconds: Per-request timeout for the owned client.
            allow_private_destinations: Forwarded to :meth:`__init__`.
            allowed_destination_ports: Forwarded to :meth:`__init__`.

        Raises:
            ValueError: ``alg`` is not ed25519 / es256, or the PEM contains
                a key whose type doesn't match ``alg``.
        """
        if alg in ("es256", "ES256"):
            alg = ALG_ES256
        elif alg == "EdDSA":
            alg = ALG_ED25519
        if alg not in (ALG_ED25519, ALG_ES256):
            raise ValueError(
                f"unsupported alg {alg!r} — use 'ed25519' or 'es256' "
                f"(the two AdCP webhook-signing algorithms)"
            )

        if isinstance(pem_path, bytes):
            pem_bytes = pem_path
        else:
            pem_bytes = Path(pem_path).read_bytes()

        private_key = load_private_key_pem(pem_bytes, password=passphrase)

        # The PEM's key type must match the requested alg — mixing them
        # would produce signatures no verifier can validate, and the
        # resulting error at delivery time would point at the receiver.
        # Fail here so the misconfiguration surfaces at construction.
        if alg == ALG_ED25519 and not isinstance(private_key, ed25519.Ed25519PrivateKey):
            raise ValueError(
                f"PEM holds a {type(private_key).__name__} but alg='ed25519' "
                f"was requested. Re-run adcp-keygen with --alg ed25519, or "
                f"pass alg='es256' to match the existing PEM."
            )
        if alg == ALG_ES256 and not isinstance(private_key, ec.EllipticCurvePrivateKey):
            raise ValueError(
                f"PEM holds a {type(private_key).__name__} but alg='es256' "
                f"was requested. Re-run adcp-keygen with --alg es256, or "
                f"pass alg='ed25519' to match the existing PEM."
            )

        return cls(
            private_key=private_key,
            key_id=key_id,
            alg=alg,
            client=client,
            timeout_seconds=timeout_seconds,
            allow_private_destinations=allow_private_destinations,
            allowed_destination_ports=allowed_destination_ports,
            transport_hooks=transport_hooks,
        )

    @classmethod
    def from_bearer_token(
        cls,
        token: str,
        *,
        client: httpx.AsyncClient | None = None,
        timeout_seconds: float = _DEFAULT_TIMEOUT_SECONDS,
        allow_private_destinations: bool = False,
        allowed_destination_ports: frozenset[int] | None = None,
        transport_hooks: tuple[TransportHook, ...] = (),
    ) -> WebhookSender:
        """Build a sender that POSTs with ``Authorization: Bearer <token>``.

        For buyers who authenticate the sender at the gateway and don't
        verify body signatures. The sender's marshaling guarantees still
        apply (byte-exact JSON, idempotency_key in body); body signing
        is skipped.

        A buyer treating bearer tokens as the sole authenticity signal
        SHOULD also enforce TLS/mTLS at the transport layer — a stolen
        token is a complete forgery. Prefer JWK signing (:meth:`from_jwk`)
        for AdCP-conformant deliveries.
        """
        if not isinstance(token, str) or not token:
            raise ValueError("bearer token must be a non-empty string")
        return cls._from_strategy(
            BearerTokenStrategy(token=token),
            key_id="bearer",
            client=client,
            timeout_seconds=timeout_seconds,
            allow_private_destinations=allow_private_destinations,
            allowed_destination_ports=allowed_destination_ports,
            transport_hooks=transport_hooks,
        )

    @classmethod
    def from_adcp_legacy_hmac(
        cls,
        secret: bytes,
        *,
        key_id: str,
        client: httpx.AsyncClient | None = None,
        timeout_seconds: float = _DEFAULT_TIMEOUT_SECONDS,
        allow_private_destinations: bool = False,
        allowed_destination_ports: frozenset[int] | None = None,
        transport_hooks: tuple[TransportHook, ...] = (),
    ) -> WebhookSender:
        """Build a sender wired to AdCP-legacy HMAC-SHA256.

        Wire format matches :func:`adcp.signing.webhook_hmac.verify_webhook_hmac`:
        ``X-AdCP-Signature: sha256=<hex>`` over ``f"{timestamp}.{body}"``,
        with ``X-AdCP-Timestamp`` set fresh per delivery (resends produce
        a new signature over the same body).

        ``secret`` is the raw HMAC key — the AdCP-legacy scheme has no
        canonical encoding, so callers pass bytes directly. ``key_id``
        is echoed in ``X-AdCP-Key-Id`` for receiver-side multi-key
        rotation; it is not used in the signature itself.

        AdCP-legacy HMAC will be removed in AdCP 4.0 — operators SHOULD
        migrate to JWK signing (:meth:`from_jwk`) ahead of that boundary.
        """
        if not isinstance(secret, bytes) or not secret:
            raise ValueError("hmac secret must be non-empty bytes")
        if not isinstance(key_id, str) or not key_id:
            raise ValueError("key_id must be a non-empty string")
        # Mirror the receiver-side _warn_once() in webhook_hmac so a
        # sender-only operator (no receiver in this process) still sees
        # the AdCP 4.0 deprecation signal at runtime, not just in the
        # docstring.
        _warn_legacy_hmac_once()
        return cls._from_strategy(
            AdcpLegacyHmacStrategy(secret=secret, key_id=key_id),
            key_id=key_id,
            client=client,
            timeout_seconds=timeout_seconds,
            allow_private_destinations=allow_private_destinations,
            allowed_destination_ports=allowed_destination_ports,
            transport_hooks=transport_hooks,
        )

    @classmethod
    def from_standard_webhooks_secret(
        cls,
        secret: str,
        *,
        key_id: str,
        client: httpx.AsyncClient | None = None,
        timeout_seconds: float = _DEFAULT_TIMEOUT_SECONDS,
        allow_private_destinations: bool = False,
        allowed_destination_ports: frozenset[int] | None = None,
        transport_hooks: tuple[TransportHook, ...] = (),
    ) -> WebhookSender:
        """Build a sender wired to standardwebhooks.com v1 (Svix/Resend interop).

        ``secret`` is the canonical ``whsec_<base64>`` form distributed
        by buyers running Svix, Resend, or any other Standard Webhooks
        verifier. The constructor base64-decodes the prefix-stripped
        payload internally — passing the literal ``whsec_...`` to
        :meth:`from_adcp_legacy_hmac` would silently produce signatures
        Svix rejects, which is exactly the footgun this typed split
        prevents.

        Wire format per spec: ``webhook-id`` / ``webhook-timestamp`` /
        ``webhook-signature: v1,<base64>`` over
        ``f"{webhook_id}.{webhook_timestamp}.{body}"``. Each delivery
        gets a fresh ``webhook-id`` so a receiver using webhook-id for
        its own replay cache doesn't false-positive on a legitimate
        retry — :meth:`resend` re-signs and gets a new id.
        """
        if not isinstance(secret, str) or not secret:
            raise ValueError("secret must be a non-empty string (whsec_<base64>)")
        if not isinstance(key_id, str) or not key_id:
            raise ValueError("key_id must be a non-empty string")
        decoded = _decode_sw_secret(secret)
        return cls._from_strategy(
            StandardWebhooksHmacStrategy(secret=decoded, key_id=key_id),
            key_id=key_id,
            client=client,
            timeout_seconds=timeout_seconds,
            allow_private_destinations=allow_private_destinations,
            allowed_destination_ports=allowed_destination_ports,
            transport_hooks=transport_hooks,
        )

    def __repr__(self) -> str:
        # Explicit repr so no future debug helper or error traceback auto-
        # renders self.__dict__ and pulls the private key (or HMAC secret /
        # bearer token) into logs.
        return f"WebhookSender(auth={type(self._auth).__name__}, " f"key_id={self._key_id!r})"

    @property
    def signs_with_rfc9421(self) -> bool:
        """``True`` iff this sender uses the RFC 9421 webhook-signing profile.

        Boot-time validators read this to enforce the
        ``webhook_signing.supported=true`` capability invariant:
        capabilities advertise RFC 9421 → wired sender must produce
        ``Signature`` / ``Signature-Input`` headers. ``from_bearer_token``,
        ``from_adcp_legacy_hmac``, and ``from_standard_webhooks_secret``
        senders return ``False``.
        """
        return isinstance(self._auth, JwkSignerStrategy)

    async def aclose(self) -> None:
        """Close the internal httpx client if we own it."""
        if self._owns_client and self._client is not None:
            await self._client.aclose()
            self._client = None

    async def __aenter__(self) -> WebhookSender:
        if not self._owns_client:
            await self._get_client()
        return self

    async def __aexit__(self, *args: Any) -> None:
        await self.aclose()

    async def _get_client(self) -> httpx.AsyncClient:
        if self._client is None:
            self._client = httpx.AsyncClient(timeout=self._timeout)
        return self._client

    async def send_mcp(
        self,
        *,
        url: str,
        task_id: str,
        status: GeneratedTaskStatus | str,
        task_type: TaskType | str,
        result: AdcpAsyncResponseData | dict[str, Any] | None = None,
        timestamp: datetime | None = None,
        operation_id: str,
        notification_id: str | None = None,
        message: str | None = None,
        context_id: str | None = None,
        protocol: AdcpProtocol | str | None = None,
        idempotency_key: str | None = None,
        token: str | None = None,
        extra_headers: Mapping[str, str] | None = None,
    ) -> WebhookDeliveryResult:
        """POST a signed MCP-style task-status webhook.

        On retry, prefer :meth:`resend` over calling this again — ``resend``
        replays the exact same bytes, whereas re-invoking ``send_mcp`` with
        the "same" args would produce a fresh ``timestamp`` and potentially
        a different serialized body, which conflicts with the delivery key's
        immutable payload binding.

        :param token: Buyer-supplied token from
            ``push_notification_config.token`` echoed back on the
            payload's ``token`` field per spec
            (``schemas/cache/core/push_notification_config.json``: "Echoed
            back in webhook payload to validate request authenticity").
            Cross-language wire-parity with the JS implementation.
        """
        return await self.send_prepared(
            self.prepare_mcp(
                url=url,
                task_id=task_id,
                status=status,
                task_type=task_type,
                result=result,
                timestamp=timestamp,
                operation_id=operation_id,
                notification_id=notification_id,
                message=message,
                context_id=context_id,
                protocol=protocol,
                idempotency_key=idempotency_key,
                token=token,
                extra_headers=extra_headers,
            )
        )

    def prepare_mcp(
        self,
        *,
        url: str,
        task_id: str,
        status: GeneratedTaskStatus | str,
        task_type: TaskType | str,
        result: AdcpAsyncResponseData | dict[str, Any] | None = None,
        timestamp: datetime | None = None,
        operation_id: str,
        notification_id: str | None = None,
        message: str | None = None,
        context_id: str | None = None,
        protocol: AdcpProtocol | str | None = None,
        idempotency_key: str | None = None,
        token: str | None = None,
        extra_headers: Mapping[str, str] | None = None,
    ) -> PreparedWebhook:
        """Prepare an MCP webhook without performing network I/O.

        Durable publishers call this inside (or immediately before) their
        outbox transaction and persist all returned fields verbatim. The
        returned body is exactly what :meth:`send_prepared` signs and posts.
        """
        payload = create_mcp_webhook_payload(
            task_id=task_id,
            status=status,
            task_type=task_type,
            result=result,
            timestamp=timestamp,
            operation_id=operation_id,
            notification_id=notification_id,
            message=message,
            context_id=context_id,
            protocol=protocol,
            idempotency_key=idempotency_key,
            token=token,
        )
        body_dict = {
            **to_wire_dict(payload),
            "idempotency_key": payload.idempotency_key,
        }
        body = json.dumps(body_dict).encode("utf-8")
        if len(body) > _MAX_BODY_BYTES:
            raise ValueError(
                f"serialized webhook body is {len(body):,} bytes, over the "
                f"{_MAX_BODY_BYTES:,}-byte cap. Split into smaller webhooks "
                "or use batch-reporting endpoints."
            )
        return PreparedWebhook(
            url=url,
            idempotency_key=payload.idempotency_key,
            body=body,
            extra_headers=dict(extra_headers) if extra_headers else {},
        )

    async def send_prepared(self, prepared: PreparedWebhook) -> WebhookDeliveryResult:
        """Sign and post a previously prepared immutable webhook request."""
        if not prepared.idempotency_key:
            raise ValueError("prepared webhook idempotency_key must be non-empty")
        if not prepared.body:
            raise ValueError("prepared webhook body must be non-empty")
        if len(prepared.body) > _MAX_BODY_BYTES:
            raise ValueError(
                f"serialized webhook body is {len(prepared.body):,} bytes, over the "
                f"{_MAX_BODY_BYTES:,}-byte cap"
            )
        try:
            payload = json.loads(prepared.body)
        except (json.JSONDecodeError, UnicodeDecodeError) as exc:
            raise ValueError("prepared webhook body must be a JSON object") from exc
        if not isinstance(payload, dict):
            raise ValueError("prepared webhook body must be a JSON object")
        if payload.get("idempotency_key") != prepared.idempotency_key:
            raise ValueError(
                "prepared webhook body idempotency_key does not match its immutable binding"
            )
        return await self._send_bytes(
            url=prepared.url,
            body=prepared.body,
            idempotency_key=prepared.idempotency_key,
            extra_headers=prepared.extra_headers or None,
        )

    async def send_revocation_notification(
        self,
        *,
        url: str,
        rights_id: str,
        brand_id: str,
        reason: str,
        effective_at: str,
        idempotency_key: str | None = None,
        extra_headers: Mapping[str, str] | None = None,
    ) -> WebhookDeliveryResult:
        """POST a signed rights-revocation notification."""
        key = idempotency_key or generate_webhook_idempotency_key()
        payload: dict[str, Any] = {
            "idempotency_key": key,
            "rights_id": rights_id,
            "brand_id": brand_id,
            "reason": reason,
            "effective_at": effective_at,
        }
        return await self.send_raw(
            url=url, idempotency_key=key, payload=payload, extra_headers=extra_headers
        )

    async def send_artifact_webhook(
        self,
        *,
        url: str,
        media_buy_id: str,
        batch_id: str,
        timestamp: str,
        artifacts: list[dict[str, Any]],
        idempotency_key: str | None = None,
        extra_headers: Mapping[str, str] | None = None,
    ) -> WebhookDeliveryResult:
        """POST a signed content-standards artifact webhook."""
        key = idempotency_key or generate_webhook_idempotency_key()
        payload: dict[str, Any] = {
            "idempotency_key": key,
            "media_buy_id": media_buy_id,
            "batch_id": batch_id,
            "timestamp": timestamp,
            "artifacts": artifacts,
        }
        return await self.send_raw(
            url=url, idempotency_key=key, payload=payload, extra_headers=extra_headers
        )

    async def send_collection_list_changed(
        self,
        *,
        url: str,
        list_id: str,
        resolved_at: str,
        signature: str,
        idempotency_key: str | None = None,
        extra_headers: Mapping[str, str] | None = None,
    ) -> WebhookDeliveryResult:
        """POST a signed governance collection-list-changed webhook.

        ``signature`` is the payload-level signature field that predates 9421
        webhook transport signing — it remains required by the schema. The
        9421 signature this method adds protects the transport envelope.
        """
        key = idempotency_key or generate_webhook_idempotency_key()
        payload: dict[str, Any] = {
            "idempotency_key": key,
            "event": "collection_list_changed",
            "list_id": list_id,
            "resolved_at": resolved_at,
            "signature": signature,
        }
        return await self.send_raw(
            url=url, idempotency_key=key, payload=payload, extra_headers=extra_headers
        )

    async def send_property_list_changed(
        self,
        *,
        url: str,
        list_id: str,
        resolved_at: str,
        signature: str,
        idempotency_key: str | None = None,
        extra_headers: Mapping[str, str] | None = None,
    ) -> WebhookDeliveryResult:
        """POST a signed governance property-list-changed webhook."""
        key = idempotency_key or generate_webhook_idempotency_key()
        payload: dict[str, Any] = {
            "idempotency_key": key,
            "event": "property_list_changed",
            "list_id": list_id,
            "resolved_at": resolved_at,
            "signature": signature,
        }
        return await self.send_raw(
            url=url, idempotency_key=key, payload=payload, extra_headers=extra_headers
        )

    async def send_wholesale_feed(
        self,
        *,
        url: str,
        subscriber_id: str,
        account_id: str,
        notification_type: str,
        wholesale_feed_version: str,
        cache_scope: str,
        event: WholesaleFeedEvent | Mapping[str, Any],
        previous_wholesale_feed_version: str | None = None,
        fired_at: datetime | None = None,
        idempotency_key: str | None = None,
        subscription_event_types: Sequence[Any] | None = None,
        extra_headers: Mapping[str, str] | None = None,
    ) -> WebhookDeliveryResult:
        """POST a signed account-scoped wholesale feed notification.

        ``subscription_event_types`` is optional but recommended when the
        caller is sending to an ``accounts[].notification_configs[]`` entry:
        pass that entry's ``event_types`` to fail closed if the subscription
        did not request this notification type.
        """

        if not isinstance(subscriber_id, str) or not subscriber_id:
            raise ValueError("subscriber_id must be a non-empty string")
        if not isinstance(account_id, str) or not account_id:
            raise ValueError("account_id must be a non-empty string")
        if not isinstance(wholesale_feed_version, str) or not wholesale_feed_version:
            raise ValueError("wholesale_feed_version must be a non-empty string")

        event_model = event
        if not isinstance(event_model, WholesaleFeedEvent):
            event_model = WholesaleFeedEvent.model_validate(event_model)
        notification_type_value = _enum_value(notification_type)
        event_type = _enum_value(event_model.event_type)
        entity_type = _enum_value(event_model.entity_type)
        if notification_type_value != event_type:
            raise ValueError(
                "notification_type must match event.event_type "
                f"(got {notification_type_value!r}, event has {event_type!r})"
            )
        if subscription_event_types is not None:
            allowed_event_types = {_enum_value(item) for item in subscription_event_types}
        else:
            allowed_event_types = None
        if allowed_event_types is not None and notification_type_value not in allowed_event_types:
            raise ValueError(
                "notification_type is not present in the subscription's event_types; "
                "sellers must not silently widen account notification filters"
            )

        expected_entity_type = _entity_type_for_wholesale_notification(notification_type_value)
        if entity_type != expected_entity_type:
            raise ValueError(
                "event.entity_type does not match notification_type "
                f"(got {entity_type!r}, expected {expected_entity_type!r})"
            )

        cache_scope_value = _enum_value(cache_scope)
        applies_to = getattr(event_model.payload, "applies_to", None)
        applies_to_scope = _enum_value(getattr(applies_to, "scope", None))
        if applies_to_scope != cache_scope_value:
            raise ValueError(
                "cache_scope must match event.payload.applies_to.scope "
                f"(got {cache_scope_value!r}, event has {applies_to_scope!r})"
            )

        key = idempotency_key or generate_webhook_idempotency_key()
        timestamp = fired_at or datetime.now(timezone.utc)
        webhook = WholesaleFeedWebhook.model_validate(
            {
                "idempotency_key": key,
                "notification_id": event_model.event_id,
                "notification_type": notification_type_value,
                "fired_at": timestamp,
                "subscriber_id": subscriber_id,
                "account_id": account_id,
                "wholesale_feed_version": wholesale_feed_version,
                "previous_wholesale_feed_version": previous_wholesale_feed_version,
                "cache_scope": cache_scope_value,
                "event": event_model,
            }
        )
        return await self.send_raw(
            url=url,
            idempotency_key=key,
            payload=webhook.model_dump(mode="json", exclude_none=True),
            extra_headers=extra_headers,
        )

    async def send_wholesale_feed_to_subscription(
        self,
        *,
        subscription: NotificationConfig | Mapping[str, Any],
        account_id: str,
        notification_type: str,
        wholesale_feed_version: str,
        cache_scope: str,
        event: WholesaleFeedEvent | Mapping[str, Any],
        previous_wholesale_feed_version: str | None = None,
        fired_at: datetime | None = None,
        idempotency_key: str | None = None,
        extra_headers: Mapping[str, str] | None = None,
    ) -> WebhookDeliveryResult:
        """POST a wholesale feed notification to a ``NotificationConfig``.

        This convenience wrapper keeps ``url``, ``subscriber_id``, and
        ``event_types`` coupled to the same persisted subscription entry.
        """

        config = (
            subscription
            if isinstance(subscription, NotificationConfig)
            else NotificationConfig.model_validate(subscription)
        )
        return await self.send_wholesale_feed(
            url=str(config.url),
            subscriber_id=config.subscriber_id,
            account_id=account_id,
            notification_type=notification_type,
            wholesale_feed_version=wholesale_feed_version,
            cache_scope=cache_scope,
            event=event,
            previous_wholesale_feed_version=previous_wholesale_feed_version,
            fired_at=fired_at,
            idempotency_key=idempotency_key,
            subscription_event_types=config.event_types,
            extra_headers=extra_headers,
        )

    async def send_webhook_challenge(
        self,
        *,
        url: str,
        account_id: str,
        subscriber_id: str,
        challenge: str | None = None,
        extra_headers: Mapping[str, str] | None = None,
    ) -> WebhookDeliveryResult:
        """POST a signed durable-subscription proof-of-control challenge.

        The body matches the durable ``notification_configs[]`` challenge
        shape and intentionally does not inject ``idempotency_key``:

        ``{"type":"webhook.challenge","challenge":"...", ...}``

        Pair this low-level sender method with
        :func:`adcp.webhooks.challenge_webhook_destination` when you also
        want URL validation and response echo checking in one call.
        """

        payload = create_webhook_challenge_payload(
            account_id=account_id,
            subscriber_id=subscriber_id,
            challenge=challenge,
        )
        challenge_value = str(payload["challenge"])
        body = json.dumps(payload, separators=(",", ":")).encode("utf-8")
        return await self._send_bytes(
            url=url,
            body=body,
            idempotency_key=challenge_value,
            extra_headers=extra_headers,
        )

    async def send_raw(
        self,
        *,
        url: str,
        idempotency_key: str,
        payload: dict[str, Any],
        extra_headers: Mapping[str, str] | None = None,
    ) -> WebhookDeliveryResult:
        """Low-level escape hatch: sign + POST an arbitrary payload.

        The ``idempotency_key`` kwarg is required and is injected into the
        payload before signing — the visible signature makes the contract
        impossible to forget, unlike a runtime dict check. If ``payload``
        already carries an ``idempotency_key``, the kwarg wins so the two
        cannot disagree.
        """
        if not isinstance(idempotency_key, str) or not idempotency_key:
            raise ValueError("idempotency_key must be a non-empty string")
        body_dict = {**payload, "idempotency_key": idempotency_key}
        # Byte-exact serialization — this is the ONLY representation that
        # gets signed AND posted. Do not allow an httpx `json=` path anywhere
        # in the stack because it would reserialize and break the digest.
        body = json.dumps(body_dict).encode("utf-8")
        if len(body) > _MAX_BODY_BYTES:
            raise ValueError(
                f"serialized webhook body is {len(body):,} bytes, over the "
                f"{_MAX_BODY_BYTES:,}-byte cap. Split into smaller webhooks "
                "or use batch-reporting endpoints."
            )
        return await self._send_bytes(
            url=url,
            body=body,
            idempotency_key=idempotency_key,
            extra_headers=extra_headers,
        )

    async def resend(self, result: WebhookDeliveryResult) -> WebhookDeliveryResult:
        """Replay an earlier delivery under a fresh signature.

        The bytes are identical (same ``idempotency_key``, same payload
        fields, same serialization) — only the Signature / Signature-Input /
        Content-Digest headers are regenerated. The receiver dedupes via
        ``idempotency_key``, so the replayed event is a spec-correct retry
        that won't cause double-processing.
        """
        if not result.sent_body:
            raise ValueError(
                "cannot resend: result has no captured sent_body (likely constructed "
                "externally). Call a send_* method on this sender first."
            )
        return await self._send_bytes(
            url=result.url,
            body=result.sent_body,
            idempotency_key=result.idempotency_key,
            extra_headers=result.sent_extra_headers or None,
        )

    async def _send_bytes(
        self,
        *,
        url: str,
        body: bytes,
        idempotency_key: str,
        extra_headers: Mapping[str, str] | None,
    ) -> WebhookDeliveryResult:
        """Sign + POST a pre-serialized body through an SSRF-validated transport.

        When the sender owns its httpx client (the default — ``client=None``
        was passed to ``__init__``), every delivery builds a per-request
        :class:`adcp.signing.ip_pinned_transport.AsyncIpPinnedTransport`
        that resolves the destination, runs the full SSRF range check
        (loopback / RFC 1918 / link-local / CGNAT / IPv6 ULA / multicast /
        cloud metadata), enforces the port allowlist, and pins the
        connection to the validated IP. This closes the DNS-rebinding
        TOCTOU between validate and connect.

        When the operator supplied their own client
        (``WebhookSender(client=...)`` — typically a vetted egress proxy
        with mTLS to a known buyer set, or an ASGI transport for testing),
        the sender trusts the operator's transport completely. Pin-and-bind
        is skipped; the operator's transport owns SSRF.

        On the owned-client path, SSRF validation runs **before** signing
        so a hostile URL is rejected without first generating an
        Ed25519/ES256 signature over the body. That signature would
        otherwise sit in process memory until the SSRF rejection —
        anything that snapshots locals on exception (faulthandler,
        custom logging) could capture it. Validate first, sign second.

        Transport hooks run before SSRF; the rewritten URL is what gets
        validated, signed, and POSTed. The signature covers the URL the
        request actually lands at, not the URL the caller typed —
        otherwise a receiver computing ``@target-uri`` from its observed
        Host header would see a different value and verification would
        fail. The hook output is bounded (hostname-only rewrite, scheme
        and port preserved), so this can't widen the destination space.
        """
        effective_url = apply_hooks(url, self._transport_hooks)

        # Build the pinned transport up-front for the owned-client path.
        # SSRF + port validation runs against the *post-hook* URL — the
        # one we'll actually connect to. A hostile URL raises
        # SSRFValidationError here and the body never gets signed (no
        # signature material to leak via faulthandler / custom logging
        # on exception).
        transport: AsyncIpPinnedTransport | None = None
        if self._owns_client:
            transport = build_async_ip_pinned_transport(
                effective_url,
                allow_private=self._allow_private_destinations,
                allowed_ports=self._allowed_destination_ports,
            )

        base_headers = {"Content-Type": "application/json", "Accept-Encoding": "identity"}
        auth_headers = self._auth.build_auth_headers(method="POST", url=effective_url, body=body)
        headers = merge_extra_headers(
            base={**base_headers, **auth_headers},
            extra=extra_headers,
            reserved=self._auth.reserved_headers(),
        )

        if transport is not None:
            # Owned-client path. ``trust_env=False`` prevents httpx from
            # routing the request through ``HTTPS_PROXY`` / ``HTTP_PROXY``
            # env vars — every other pinned-transport callsite in the
            # codebase sets this for the same reason (default_jwks_fetcher,
            # async_default_jwks_fetcher, revocation_fetcher). Without it,
            # an attacker who controls process env can route the signed
            # webhook through their endpoint, defeating the IP pin entirely.
            async with httpx.AsyncClient(
                transport=transport,
                timeout=self._timeout,
                follow_redirects=False,
                trust_env=False,
            ) as client:
                status_code, response_headers, response_body = await _post_with_bounded_response(
                    client,
                    effective_url,
                    body=body,
                    headers=headers,
                )
        else:
            # Operator-supplied client — they own the SSRF guarantees on
            # their transport (proxy allowlist, mTLS, etc.). Reachable as
            # None after aclose(); explicit raise survives ``python -O``
            # which would strip an assert.
            if self._client is None:
                raise RuntimeError(
                    "WebhookSender's operator-supplied client was already "
                    "closed. Construct a new sender or pass a fresh client."
                )
            status_code, response_headers, response_body = await _post_with_bounded_response(
                self._client,
                effective_url,
                body=body,
                headers=headers,
            )

        return WebhookDeliveryResult(
            status_code=status_code,
            idempotency_key=idempotency_key,
            url=effective_url,
            response_headers=response_headers,
            response_body=response_body,
            sent_body=body,
            sent_extra_headers=dict(extra_headers) if extra_headers else {},
        )

Outbound signed-webhook delivery client.

Owns one webhook-signing private key. Reuses a single :class:httpx.AsyncClient across requests for connection pooling — pass your own via client= if you want to share it with other SDK surfaces.

Thread/task safety: safe to call concurrent send_* from many asyncio tasks. The underlying httpx.AsyncClient manages its own pool.

Construct a sender wired to RFC 9421 JWK signing.

The HMAC and bearer modes are reached via :meth:from_bearer_token, :meth:from_adcp_legacy_hmac, and :meth:from_standard_webhooks_secret — those classmethods bypass this initializer through :meth:_from_strategy because their key material has different types (bytes / str rather than PrivateKey).

transport_hooks runs URL rewrites before SSRF validation — see :class:DockerLocalhostRewrite for the canonical use case. SSRF remains authoritative on the rewritten URL; hooks cannot punch through the range check.

Static methods

def from_adcp_legacy_hmac(secret: bytes,
*,
key_id: str,
client: httpx.AsyncClient | None = None,
timeout_seconds: float = 10.0,
allow_private_destinations: bool = False,
allowed_destination_ports: frozenset[int] | None = None,
transport_hooks: tuple[TransportHook, ...] = ()) ‑> WebhookSender

Build a sender wired to AdCP-legacy HMAC-SHA256.

Wire format matches :func:verify_webhook_hmac(): X-AdCP-Signature: sha256=<hex> over f"{timestamp}.{body}", with X-AdCP-Timestamp set fresh per delivery (resends produce a new signature over the same body).

secret is the raw HMAC key — the AdCP-legacy scheme has no canonical encoding, so callers pass bytes directly. key_id is echoed in X-AdCP-Key-Id for receiver-side multi-key rotation; it is not used in the signature itself.

AdCP-legacy HMAC will be removed in AdCP 4.0 — operators SHOULD migrate to JWK signing (:meth:from_jwk) ahead of that boundary.

def from_bearer_token(token: str,
*,
client: httpx.AsyncClient | None = None,
timeout_seconds: float = 10.0,
allow_private_destinations: bool = False,
allowed_destination_ports: frozenset[int] | None = None,
transport_hooks: tuple[TransportHook, ...] = ()) ‑> WebhookSender

Build a sender that POSTs with Authorization: Bearer <token>.

For buyers who authenticate the sender at the gateway and don't verify body signatures. The sender's marshaling guarantees still apply (byte-exact JSON, idempotency_key in body); body signing is skipped.

A buyer treating bearer tokens as the sole authenticity signal SHOULD also enforce TLS/mTLS at the transport layer — a stolen token is a complete forgery. Prefer JWK signing (:meth:from_jwk) for AdCP-conformant deliveries.

def from_jwk(jwk: Mapping[str, Any],
*,
d_field: str = 'd',
client: httpx.AsyncClient | None = None,
timeout_seconds: float = 10.0,
allow_private_destinations: bool = False,
allowed_destination_ports: frozenset[int] | None = None,
transport_hooks: tuple[TransportHook, ...] = ()) ‑> WebhookSender

Construct from a JWK that includes the private scalar.

The JWK MUST have adcp_use == "webhook-signing" — the sender doesn't validate this (you're signing with your own key; validation happens at the receiver), but a key whose adcp_use is wrong will be rejected by every conformant verifier.

allow_private_destinations and allowed_destination_ports forward to :meth:__init__ — see that signature for semantics.

def from_pem(pem_path: str | Path | bytes,
*,
key_id: str,
alg: str = 'ed25519',
passphrase: bytes | None = None,
client: httpx.AsyncClient | None = None,
timeout_seconds: float = 10.0,
allow_private_destinations: bool = False,
allowed_destination_ports: frozenset[int] | None = None,
transport_hooks: tuple[TransportHook, ...] = ()) ‑> WebhookSender

Load a private key from a PEM file and bind it as a webhook sender.

Companion to adcp-keygen --purpose webhook-signing, which writes the PEM and prints the public JWK. The JWK is published at your jwks_uri; the PEM holds the private key material. from_pem reads the PEM, constructs the right PrivateKey type for alg, and returns a sender ready to send.

Args
-----=
pem_path
Path to the PKCS#8 PEM, or the PEM bytes directly.
key_id
JWK kid claim — must match the published JWK.
alg
Signature algorithm. ed25519 (default) or es256. Also accepts the RFC 9421 form ecdsa-p256-sha256.
passphrase
Required if the PEM is encrypted (adcp-keygen --encrypt).
client
Optional pre-built :class:httpx.AsyncClient to share across the SDK; the sender owns its own client when omitted.
timeout_seconds
Per-request timeout for the owned client.
allow_private_destinations
Forwarded to :meth:__init__.
allowed_destination_ports
Forwarded to :meth:__init__.
Raises
-----=
ValueError
alg is not ed25519 / es256, or the PEM contains a key whose type doesn't match alg.
def from_standard_webhooks_secret(secret: str,
*,
key_id: str,
client: httpx.AsyncClient | None = None,
timeout_seconds: float = 10.0,
allow_private_destinations: bool = False,
allowed_destination_ports: frozenset[int] | None = None,
transport_hooks: tuple[TransportHook, ...] = ()) ‑> WebhookSender

Build a sender wired to standardwebhooks.com v1 (Svix/Resend interop).

secret is the canonical whsec_<base64> form distributed by buyers running Svix, Resend, or any other Standard Webhooks verifier. The constructor base64-decodes the prefix-stripped payload internally — passing the literal whsec_… to :meth:from_adcp_legacy_hmac would silently produce signatures Svix rejects, which is exactly the footgun this typed split prevents.

Wire format per spec: webhook-id / webhook-timestamp / webhook-signature: v1,<base64> over f"{webhook_id}.{webhook_timestamp}.{body}". Each delivery gets a fresh webhook-id so a receiver using webhook-id for its own replay cache doesn't false-positive on a legitimate retry — :meth:resend re-signs and gets a new id.

Instance variables

prop signs_with_rfc9421 : bool
Expand source code
@property
def signs_with_rfc9421(self) -> bool:
    """``True`` iff this sender uses the RFC 9421 webhook-signing profile.

    Boot-time validators read this to enforce the
    ``webhook_signing.supported=true`` capability invariant:
    capabilities advertise RFC 9421 → wired sender must produce
    ``Signature`` / ``Signature-Input`` headers. ``from_bearer_token``,
    ``from_adcp_legacy_hmac``, and ``from_standard_webhooks_secret``
    senders return ``False``.
    """
    return isinstance(self._auth, JwkSignerStrategy)

True iff this sender uses the RFC 9421 webhook-signing profile.

Boot-time validators read this to enforce the webhook_signing.supported=true capability invariant: capabilities advertise RFC 9421 → wired sender must produce Signature / Signature-Input headers. from_bearer_token, from_adcp_legacy_hmac, and from_standard_webhooks_secret senders return False.

Methods

async def aclose(self) ‑> None
Expand source code
async def aclose(self) -> None:
    """Close the internal httpx client if we own it."""
    if self._owns_client and self._client is not None:
        await self._client.aclose()
        self._client = None

Close the internal httpx client if we own it.

def prepare_mcp(self,
*,
url: str,
task_id: str,
status: TaskStatus | str,
task_type: TaskType | str,
result: AdcpAsyncResponseData | dict[str, Any] | None = None,
timestamp: datetime | None = None,
operation_id: str,
notification_id: str | None = None,
message: str | None = None,
context_id: str | None = None,
protocol: AdcpProtocol | str | None = None,
idempotency_key: str | None = None,
token: str | None = None,
extra_headers: Mapping[str, str] | None = None) ‑> PreparedWebhook
Expand source code
def prepare_mcp(
    self,
    *,
    url: str,
    task_id: str,
    status: GeneratedTaskStatus | str,
    task_type: TaskType | str,
    result: AdcpAsyncResponseData | dict[str, Any] | None = None,
    timestamp: datetime | None = None,
    operation_id: str,
    notification_id: str | None = None,
    message: str | None = None,
    context_id: str | None = None,
    protocol: AdcpProtocol | str | None = None,
    idempotency_key: str | None = None,
    token: str | None = None,
    extra_headers: Mapping[str, str] | None = None,
) -> PreparedWebhook:
    """Prepare an MCP webhook without performing network I/O.

    Durable publishers call this inside (or immediately before) their
    outbox transaction and persist all returned fields verbatim. The
    returned body is exactly what :meth:`send_prepared` signs and posts.
    """
    payload = create_mcp_webhook_payload(
        task_id=task_id,
        status=status,
        task_type=task_type,
        result=result,
        timestamp=timestamp,
        operation_id=operation_id,
        notification_id=notification_id,
        message=message,
        context_id=context_id,
        protocol=protocol,
        idempotency_key=idempotency_key,
        token=token,
    )
    body_dict = {
        **to_wire_dict(payload),
        "idempotency_key": payload.idempotency_key,
    }
    body = json.dumps(body_dict).encode("utf-8")
    if len(body) > _MAX_BODY_BYTES:
        raise ValueError(
            f"serialized webhook body is {len(body):,} bytes, over the "
            f"{_MAX_BODY_BYTES:,}-byte cap. Split into smaller webhooks "
            "or use batch-reporting endpoints."
        )
    return PreparedWebhook(
        url=url,
        idempotency_key=payload.idempotency_key,
        body=body,
        extra_headers=dict(extra_headers) if extra_headers else {},
    )

Prepare an MCP webhook without performing network I/O.

Durable publishers call this inside (or immediately before) their outbox transaction and persist all returned fields verbatim. The returned body is exactly what :meth:send_prepared signs and posts.

async def resend(self, result: WebhookDeliveryResult) ‑> WebhookDeliveryResult
Expand source code
async def resend(self, result: WebhookDeliveryResult) -> WebhookDeliveryResult:
    """Replay an earlier delivery under a fresh signature.

    The bytes are identical (same ``idempotency_key``, same payload
    fields, same serialization) — only the Signature / Signature-Input /
    Content-Digest headers are regenerated. The receiver dedupes via
    ``idempotency_key``, so the replayed event is a spec-correct retry
    that won't cause double-processing.
    """
    if not result.sent_body:
        raise ValueError(
            "cannot resend: result has no captured sent_body (likely constructed "
            "externally). Call a send_* method on this sender first."
        )
    return await self._send_bytes(
        url=result.url,
        body=result.sent_body,
        idempotency_key=result.idempotency_key,
        extra_headers=result.sent_extra_headers or None,
    )

Replay an earlier delivery under a fresh signature.

The bytes are identical (same idempotency_key, same payload fields, same serialization) — only the Signature / Signature-Input / Content-Digest headers are regenerated. The receiver dedupes via idempotency_key, so the replayed event is a spec-correct retry that won't cause double-processing.

async def send_artifact_webhook(self,
*,
url: str,
media_buy_id: str,
batch_id: str,
timestamp: str,
artifacts: list[dict[str, Any]],
idempotency_key: str | None = None,
extra_headers: Mapping[str, str] | None = None) ‑> WebhookDeliveryResult
Expand source code
async def send_artifact_webhook(
    self,
    *,
    url: str,
    media_buy_id: str,
    batch_id: str,
    timestamp: str,
    artifacts: list[dict[str, Any]],
    idempotency_key: str | None = None,
    extra_headers: Mapping[str, str] | None = None,
) -> WebhookDeliveryResult:
    """POST a signed content-standards artifact webhook."""
    key = idempotency_key or generate_webhook_idempotency_key()
    payload: dict[str, Any] = {
        "idempotency_key": key,
        "media_buy_id": media_buy_id,
        "batch_id": batch_id,
        "timestamp": timestamp,
        "artifacts": artifacts,
    }
    return await self.send_raw(
        url=url, idempotency_key=key, payload=payload, extra_headers=extra_headers
    )

POST a signed content-standards artifact webhook.

async def send_collection_list_changed(self,
*,
url: str,
list_id: str,
resolved_at: str,
signature: str,
idempotency_key: str | None = None,
extra_headers: Mapping[str, str] | None = None) ‑> WebhookDeliveryResult
Expand source code
async def send_collection_list_changed(
    self,
    *,
    url: str,
    list_id: str,
    resolved_at: str,
    signature: str,
    idempotency_key: str | None = None,
    extra_headers: Mapping[str, str] | None = None,
) -> WebhookDeliveryResult:
    """POST a signed governance collection-list-changed webhook.

    ``signature`` is the payload-level signature field that predates 9421
    webhook transport signing — it remains required by the schema. The
    9421 signature this method adds protects the transport envelope.
    """
    key = idempotency_key or generate_webhook_idempotency_key()
    payload: dict[str, Any] = {
        "idempotency_key": key,
        "event": "collection_list_changed",
        "list_id": list_id,
        "resolved_at": resolved_at,
        "signature": signature,
    }
    return await self.send_raw(
        url=url, idempotency_key=key, payload=payload, extra_headers=extra_headers
    )

POST a signed governance collection-list-changed webhook.

signature is the payload-level signature field that predates 9421 webhook transport signing — it remains required by the schema. The 9421 signature this method adds protects the transport envelope.

async def send_mcp(self,
*,
url: str,
task_id: str,
status: TaskStatus | str,
task_type: TaskType | str,
result: AdcpAsyncResponseData | dict[str, Any] | None = None,
timestamp: datetime | None = None,
operation_id: str,
notification_id: str | None = None,
message: str | None = None,
context_id: str | None = None,
protocol: AdcpProtocol | str | None = None,
idempotency_key: str | None = None,
token: str | None = None,
extra_headers: Mapping[str, str] | None = None) ‑> WebhookDeliveryResult
Expand source code
async def send_mcp(
    self,
    *,
    url: str,
    task_id: str,
    status: GeneratedTaskStatus | str,
    task_type: TaskType | str,
    result: AdcpAsyncResponseData | dict[str, Any] | None = None,
    timestamp: datetime | None = None,
    operation_id: str,
    notification_id: str | None = None,
    message: str | None = None,
    context_id: str | None = None,
    protocol: AdcpProtocol | str | None = None,
    idempotency_key: str | None = None,
    token: str | None = None,
    extra_headers: Mapping[str, str] | None = None,
) -> WebhookDeliveryResult:
    """POST a signed MCP-style task-status webhook.

    On retry, prefer :meth:`resend` over calling this again — ``resend``
    replays the exact same bytes, whereas re-invoking ``send_mcp`` with
    the "same" args would produce a fresh ``timestamp`` and potentially
    a different serialized body, which conflicts with the delivery key's
    immutable payload binding.

    :param token: Buyer-supplied token from
        ``push_notification_config.token`` echoed back on the
        payload's ``token`` field per spec
        (``schemas/cache/core/push_notification_config.json``: "Echoed
        back in webhook payload to validate request authenticity").
        Cross-language wire-parity with the JS implementation.
    """
    return await self.send_prepared(
        self.prepare_mcp(
            url=url,
            task_id=task_id,
            status=status,
            task_type=task_type,
            result=result,
            timestamp=timestamp,
            operation_id=operation_id,
            notification_id=notification_id,
            message=message,
            context_id=context_id,
            protocol=protocol,
            idempotency_key=idempotency_key,
            token=token,
            extra_headers=extra_headers,
        )
    )

POST a signed MCP-style task-status webhook.

On retry, prefer :meth:resend over calling this again — resend replays the exact same bytes, whereas re-invoking send_mcp with the "same" args would produce a fresh timestamp and potentially a different serialized body, which conflicts with the delivery key's immutable payload binding.

:param token: Buyer-supplied token from push_notification_config.token echoed back on the payload's token field per spec (schemas/cache/core/push_notification_config.json: "Echoed back in webhook payload to validate request authenticity"). Cross-language wire-parity with the JS implementation.

async def send_prepared(self,
prepared: PreparedWebhook) ‑> WebhookDeliveryResult
Expand source code
async def send_prepared(self, prepared: PreparedWebhook) -> WebhookDeliveryResult:
    """Sign and post a previously prepared immutable webhook request."""
    if not prepared.idempotency_key:
        raise ValueError("prepared webhook idempotency_key must be non-empty")
    if not prepared.body:
        raise ValueError("prepared webhook body must be non-empty")
    if len(prepared.body) > _MAX_BODY_BYTES:
        raise ValueError(
            f"serialized webhook body is {len(prepared.body):,} bytes, over the "
            f"{_MAX_BODY_BYTES:,}-byte cap"
        )
    try:
        payload = json.loads(prepared.body)
    except (json.JSONDecodeError, UnicodeDecodeError) as exc:
        raise ValueError("prepared webhook body must be a JSON object") from exc
    if not isinstance(payload, dict):
        raise ValueError("prepared webhook body must be a JSON object")
    if payload.get("idempotency_key") != prepared.idempotency_key:
        raise ValueError(
            "prepared webhook body idempotency_key does not match its immutable binding"
        )
    return await self._send_bytes(
        url=prepared.url,
        body=prepared.body,
        idempotency_key=prepared.idempotency_key,
        extra_headers=prepared.extra_headers or None,
    )

Sign and post a previously prepared immutable webhook request.

async def send_property_list_changed(self,
*,
url: str,
list_id: str,
resolved_at: str,
signature: str,
idempotency_key: str | None = None,
extra_headers: Mapping[str, str] | None = None) ‑> WebhookDeliveryResult
Expand source code
async def send_property_list_changed(
    self,
    *,
    url: str,
    list_id: str,
    resolved_at: str,
    signature: str,
    idempotency_key: str | None = None,
    extra_headers: Mapping[str, str] | None = None,
) -> WebhookDeliveryResult:
    """POST a signed governance property-list-changed webhook."""
    key = idempotency_key or generate_webhook_idempotency_key()
    payload: dict[str, Any] = {
        "idempotency_key": key,
        "event": "property_list_changed",
        "list_id": list_id,
        "resolved_at": resolved_at,
        "signature": signature,
    }
    return await self.send_raw(
        url=url, idempotency_key=key, payload=payload, extra_headers=extra_headers
    )

POST a signed governance property-list-changed webhook.

async def send_raw(self,
*,
url: str,
idempotency_key: str,
payload: dict[str, Any],
extra_headers: Mapping[str, str] | None = None) ‑> WebhookDeliveryResult
Expand source code
async def send_raw(
    self,
    *,
    url: str,
    idempotency_key: str,
    payload: dict[str, Any],
    extra_headers: Mapping[str, str] | None = None,
) -> WebhookDeliveryResult:
    """Low-level escape hatch: sign + POST an arbitrary payload.

    The ``idempotency_key`` kwarg is required and is injected into the
    payload before signing — the visible signature makes the contract
    impossible to forget, unlike a runtime dict check. If ``payload``
    already carries an ``idempotency_key``, the kwarg wins so the two
    cannot disagree.
    """
    if not isinstance(idempotency_key, str) or not idempotency_key:
        raise ValueError("idempotency_key must be a non-empty string")
    body_dict = {**payload, "idempotency_key": idempotency_key}
    # Byte-exact serialization — this is the ONLY representation that
    # gets signed AND posted. Do not allow an httpx `json=` path anywhere
    # in the stack because it would reserialize and break the digest.
    body = json.dumps(body_dict).encode("utf-8")
    if len(body) > _MAX_BODY_BYTES:
        raise ValueError(
            f"serialized webhook body is {len(body):,} bytes, over the "
            f"{_MAX_BODY_BYTES:,}-byte cap. Split into smaller webhooks "
            "or use batch-reporting endpoints."
        )
    return await self._send_bytes(
        url=url,
        body=body,
        idempotency_key=idempotency_key,
        extra_headers=extra_headers,
    )

Low-level escape hatch: sign + POST an arbitrary payload.

The idempotency_key kwarg is required and is injected into the payload before signing — the visible signature makes the contract impossible to forget, unlike a runtime dict check. If payload already carries an idempotency_key, the kwarg wins so the two cannot disagree.

async def send_revocation_notification(self,
*,
url: str,
rights_id: str,
brand_id: str,
reason: str,
effective_at: str,
idempotency_key: str | None = None,
extra_headers: Mapping[str, str] | None = None) ‑> WebhookDeliveryResult
Expand source code
async def send_revocation_notification(
    self,
    *,
    url: str,
    rights_id: str,
    brand_id: str,
    reason: str,
    effective_at: str,
    idempotency_key: str | None = None,
    extra_headers: Mapping[str, str] | None = None,
) -> WebhookDeliveryResult:
    """POST a signed rights-revocation notification."""
    key = idempotency_key or generate_webhook_idempotency_key()
    payload: dict[str, Any] = {
        "idempotency_key": key,
        "rights_id": rights_id,
        "brand_id": brand_id,
        "reason": reason,
        "effective_at": effective_at,
    }
    return await self.send_raw(
        url=url, idempotency_key=key, payload=payload, extra_headers=extra_headers
    )

POST a signed rights-revocation notification.

async def send_webhook_challenge(self,
*,
url: str,
account_id: str,
subscriber_id: str,
challenge: str | None = None,
extra_headers: Mapping[str, str] | None = None) ‑> WebhookDeliveryResult
Expand source code
async def send_webhook_challenge(
    self,
    *,
    url: str,
    account_id: str,
    subscriber_id: str,
    challenge: str | None = None,
    extra_headers: Mapping[str, str] | None = None,
) -> WebhookDeliveryResult:
    """POST a signed durable-subscription proof-of-control challenge.

    The body matches the durable ``notification_configs[]`` challenge
    shape and intentionally does not inject ``idempotency_key``:

    ``{"type":"webhook.challenge","challenge":"...", ...}``

    Pair this low-level sender method with
    :func:`adcp.webhooks.challenge_webhook_destination` when you also
    want URL validation and response echo checking in one call.
    """

    payload = create_webhook_challenge_payload(
        account_id=account_id,
        subscriber_id=subscriber_id,
        challenge=challenge,
    )
    challenge_value = str(payload["challenge"])
    body = json.dumps(payload, separators=(",", ":")).encode("utf-8")
    return await self._send_bytes(
        url=url,
        body=body,
        idempotency_key=challenge_value,
        extra_headers=extra_headers,
    )

POST a signed durable-subscription proof-of-control challenge.

The body matches the durable notification_configs[] challenge shape and intentionally does not inject idempotency_key:

{"type":"webhook.challenge","challenge":"...", ...}

Pair this low-level sender method with :func:challenge_webhook_destination() when you also want URL validation and response echo checking in one call.

async def send_wholesale_feed(self,
*,
url: str,
subscriber_id: str,
account_id: str,
notification_type: str,
wholesale_feed_version: str,
cache_scope: str,
event: WholesaleFeedEvent | Mapping[str, Any],
previous_wholesale_feed_version: str | None = None,
fired_at: datetime | None = None,
idempotency_key: str | None = None,
subscription_event_types: Sequence[Any] | None = None,
extra_headers: Mapping[str, str] | None = None) ‑> WebhookDeliveryResult
Expand source code
async def send_wholesale_feed(
    self,
    *,
    url: str,
    subscriber_id: str,
    account_id: str,
    notification_type: str,
    wholesale_feed_version: str,
    cache_scope: str,
    event: WholesaleFeedEvent | Mapping[str, Any],
    previous_wholesale_feed_version: str | None = None,
    fired_at: datetime | None = None,
    idempotency_key: str | None = None,
    subscription_event_types: Sequence[Any] | None = None,
    extra_headers: Mapping[str, str] | None = None,
) -> WebhookDeliveryResult:
    """POST a signed account-scoped wholesale feed notification.

    ``subscription_event_types`` is optional but recommended when the
    caller is sending to an ``accounts[].notification_configs[]`` entry:
    pass that entry's ``event_types`` to fail closed if the subscription
    did not request this notification type.
    """

    if not isinstance(subscriber_id, str) or not subscriber_id:
        raise ValueError("subscriber_id must be a non-empty string")
    if not isinstance(account_id, str) or not account_id:
        raise ValueError("account_id must be a non-empty string")
    if not isinstance(wholesale_feed_version, str) or not wholesale_feed_version:
        raise ValueError("wholesale_feed_version must be a non-empty string")

    event_model = event
    if not isinstance(event_model, WholesaleFeedEvent):
        event_model = WholesaleFeedEvent.model_validate(event_model)
    notification_type_value = _enum_value(notification_type)
    event_type = _enum_value(event_model.event_type)
    entity_type = _enum_value(event_model.entity_type)
    if notification_type_value != event_type:
        raise ValueError(
            "notification_type must match event.event_type "
            f"(got {notification_type_value!r}, event has {event_type!r})"
        )
    if subscription_event_types is not None:
        allowed_event_types = {_enum_value(item) for item in subscription_event_types}
    else:
        allowed_event_types = None
    if allowed_event_types is not None and notification_type_value not in allowed_event_types:
        raise ValueError(
            "notification_type is not present in the subscription's event_types; "
            "sellers must not silently widen account notification filters"
        )

    expected_entity_type = _entity_type_for_wholesale_notification(notification_type_value)
    if entity_type != expected_entity_type:
        raise ValueError(
            "event.entity_type does not match notification_type "
            f"(got {entity_type!r}, expected {expected_entity_type!r})"
        )

    cache_scope_value = _enum_value(cache_scope)
    applies_to = getattr(event_model.payload, "applies_to", None)
    applies_to_scope = _enum_value(getattr(applies_to, "scope", None))
    if applies_to_scope != cache_scope_value:
        raise ValueError(
            "cache_scope must match event.payload.applies_to.scope "
            f"(got {cache_scope_value!r}, event has {applies_to_scope!r})"
        )

    key = idempotency_key or generate_webhook_idempotency_key()
    timestamp = fired_at or datetime.now(timezone.utc)
    webhook = WholesaleFeedWebhook.model_validate(
        {
            "idempotency_key": key,
            "notification_id": event_model.event_id,
            "notification_type": notification_type_value,
            "fired_at": timestamp,
            "subscriber_id": subscriber_id,
            "account_id": account_id,
            "wholesale_feed_version": wholesale_feed_version,
            "previous_wholesale_feed_version": previous_wholesale_feed_version,
            "cache_scope": cache_scope_value,
            "event": event_model,
        }
    )
    return await self.send_raw(
        url=url,
        idempotency_key=key,
        payload=webhook.model_dump(mode="json", exclude_none=True),
        extra_headers=extra_headers,
    )

POST a signed account-scoped wholesale feed notification.

subscription_event_types is optional but recommended when the caller is sending to an accounts[].notification_configs[] entry: pass that entry's event_types to fail closed if the subscription did not request this notification type.

async def send_wholesale_feed_to_subscription(self,
*,
subscription: NotificationConfig | Mapping[str, Any],
account_id: str,
notification_type: str,
wholesale_feed_version: str,
cache_scope: str,
event: WholesaleFeedEvent | Mapping[str, Any],
previous_wholesale_feed_version: str | None = None,
fired_at: datetime | None = None,
idempotency_key: str | None = None,
extra_headers: Mapping[str, str] | None = None) ‑> WebhookDeliveryResult
Expand source code
async def send_wholesale_feed_to_subscription(
    self,
    *,
    subscription: NotificationConfig | Mapping[str, Any],
    account_id: str,
    notification_type: str,
    wholesale_feed_version: str,
    cache_scope: str,
    event: WholesaleFeedEvent | Mapping[str, Any],
    previous_wholesale_feed_version: str | None = None,
    fired_at: datetime | None = None,
    idempotency_key: str | None = None,
    extra_headers: Mapping[str, str] | None = None,
) -> WebhookDeliveryResult:
    """POST a wholesale feed notification to a ``NotificationConfig``.

    This convenience wrapper keeps ``url``, ``subscriber_id``, and
    ``event_types`` coupled to the same persisted subscription entry.
    """

    config = (
        subscription
        if isinstance(subscription, NotificationConfig)
        else NotificationConfig.model_validate(subscription)
    )
    return await self.send_wholesale_feed(
        url=str(config.url),
        subscriber_id=config.subscriber_id,
        account_id=account_id,
        notification_type=notification_type,
        wholesale_feed_version=wholesale_feed_version,
        cache_scope=cache_scope,
        event=event,
        previous_wholesale_feed_version=previous_wholesale_feed_version,
        fired_at=fired_at,
        idempotency_key=idempotency_key,
        subscription_event_types=config.event_types,
        extra_headers=extra_headers,
    )

POST a wholesale feed notification to a NotificationConfig.

This convenience wrapper keeps url, subscriber_id, and event_types coupled to the same persisted subscription entry.

class WebhookVerifyOptions (*,
jwks_resolver: JwksResolver,
replay_store: ReplayStore | None = <factory>,
revocation_checker: RevocationChecker | None = None,
revocation_list: RevocationList | None = None,
max_skew_seconds: int = 60,
max_window_seconds: int = 300,
label: str = 'sig1',
allowed_algs: frozenset[str] = frozenset({'ed25519', 'ecdsa-p256-sha256'}),
sender_url: str | None = None,
expected_key_origins: Mapping[str, str] | None = None,
posture: str | None = None,
clock: Callable[[], float] = <built-in function time>)
Expand source code
@dataclass(frozen=True, kw_only=True)
class WebhookVerifyOptions:
    """Options for the webhook verifier.

    Subset of :class:`VerifyOptions` — several fields are pinned (tag, adcp_use,
    content-digest policy) because the webhook profile doesn't leave them as
    caller choices.

    Unlike the request verifier, there is no ``now`` field — the webhook
    verifier stamps time-of-check itself, so the same :class:`WebhookVerifyOptions`
    instance can live for the lifetime of your receiver without a factory
    closure around it. Override via ``clock=`` for deterministic tests.

    ``replay_store`` defaults to a per-options in-memory store so captured
    signatures are rejected without extra configuration. Multi-process
    receivers should supply a shared store. Passing ``None`` explicitly is
    the opt-out for specialized tests or externally enforced replay policy.
    """

    jwks_resolver: JwksResolver
    replay_store: ReplayStore | None = field(default_factory=InMemoryReplayStore)
    revocation_checker: RevocationChecker | None = None
    revocation_list: RevocationList | None = None
    max_skew_seconds: int = DEFAULT_SKEW_SECONDS
    max_window_seconds: int = MAX_WINDOW_SECONDS
    label: str = SIG_LABEL_DEFAULT
    allowed_algs: frozenset[str] = ALLOWED_ALGS
    sender_url: str | None = None
    expected_key_origins: Mapping[str, str] | None = None
    posture: str | None = None
    clock: Callable[[], float] = time.time

Options for the webhook verifier.

Subset of :class:VerifyOptions — several fields are pinned (tag, adcp_use, content-digest policy) because the webhook profile doesn't leave them as caller choices.

Unlike the request verifier, there is no now field — the webhook verifier stamps time-of-check itself, so the same :class:WebhookVerifyOptions instance can live for the lifetime of your receiver without a factory closure around it. Override via clock= for deterministic tests.

replay_store defaults to a per-options in-memory store so captured signatures are rejected without extra configuration. Multi-process receivers should supply a shared store. Passing None explicitly is the opt-out for specialized tests or externally enforced replay policy.

Instance variables

var allowed_algs : frozenset[str]
var expected_key_origins : collections.abc.Mapping[str, str] | None
var jwks_resolverJwksResolver
var label : str
var max_skew_seconds : int
var max_window_seconds : int
var posture : str | None
var replay_storeReplayStore | None
var revocation_checkerRevocationChecker | None
var revocation_listRevocationList | None
var sender_url : str | None

Methods

def clock(...) ‑> Callable[[], float]

time() -> floating point number

Return the current time in seconds since the Epoch. Fractions of a second may be present if the system clock provides them.

class WholesaleFeedEvent (root: RootModelRootType = PydanticUndefined, **data)
Expand source code
class WholesaleFeedEvent(
    RootModel[
        WholesaleFeedEvent1
        | WholesaleFeedEvent2
        | WholesaleFeedEvent3
        | WholesaleFeedEvent4
        | WholesaleFeedEvent5
        | WholesaleFeedEvent6
        | WholesaleFeedEvent7
        | WholesaleFeedEvent8
        | WholesaleFeedEvent9
    ]
):
    root: Annotated[
        WholesaleFeedEvent1
        | WholesaleFeedEvent2
        | WholesaleFeedEvent3
        | WholesaleFeedEvent4
        | WholesaleFeedEvent5
        | WholesaleFeedEvent6
        | WholesaleFeedEvent7
        | WholesaleFeedEvent8
        | WholesaleFeedEvent9,
        Field(
            description="A single change event emitted by an AdCP agent's wholesale product feed or wholesale signals feed and delivered inside wholesale-feed-webhook payloads. Events are denormalized so consumers can update local state without polling list_products / get_signals. This is distinct from buyer-provided feeds managed by sync_catalogs. The discriminator is event_type; each branch defines the payload shape.",
            discriminator='event_type',
            title='Wholesale Feed Event',
        ),
    ]
    def __getattr__(self, name: str) -> Any:
        """Proxy attribute access to the wrapped type."""
        if name.startswith('_'):
            raise AttributeError(name)
        return getattr(self.root, name)

Usage Documentation

RootModel and Custom Root Types

A Pydantic BaseModel for the root object of the model.

Attributes
-----=
root
The root object of the model.
__pydantic_root_model__
Whether the model is a RootModel.
__pydantic_private__
Private fields in the model.
__pydantic_extra__
Extra fields in the model.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

  • pydantic.root_model.RootModel[Union[WholesaleFeedEvent1, WholesaleFeedEvent2, WholesaleFeedEvent3, WholesaleFeedEvent4, WholesaleFeedEvent5, WholesaleFeedEvent6, WholesaleFeedEvent7, WholesaleFeedEvent8, WholesaleFeedEvent9]]
  • pydantic.root_model.RootModel
  • pydantic.main.BaseModel
  • typing.Generic

Class variables

var model_config
var root : adcp.types.generated_poc.core.wholesale_feed_event.WholesaleFeedEvent1 | adcp.types.generated_poc.core.wholesale_feed_event.WholesaleFeedEvent2 | adcp.types.generated_poc.core.wholesale_feed_event.WholesaleFeedEvent3 | adcp.types.generated_poc.core.wholesale_feed_event.WholesaleFeedEvent4 | adcp.types.generated_poc.core.wholesale_feed_event.WholesaleFeedEvent5 | adcp.types.generated_poc.core.wholesale_feed_event.WholesaleFeedEvent6 | adcp.types.generated_poc.core.wholesale_feed_event.WholesaleFeedEvent7 | adcp.types.generated_poc.core.wholesale_feed_event.WholesaleFeedEvent8 | adcp.types.generated_poc.core.wholesale_feed_event.WholesaleFeedEvent9
class WholesaleFeedWebhook (**data: Any)
Expand source code
class WholesaleFeedWebhook(AdCPBaseModel):
    model_config = ConfigDict(
        extra='forbid',
    )
    idempotency_key: Annotated[
        str,
        Field(
            description='Sender-generated key stable across retries of the same webhook fire. Receivers MUST dedupe by this key, scoped to the authenticated sender identity.',
            max_length=255,
            min_length=16,
            pattern='^[A-Za-z0-9_.:-]{16,255}$',
        ),
    ]
    notification_id: Annotated[
        UUID,
        Field(
            description='Stable identifier for this logical wholesale feed event. MUST equal event.event_id. Re-emissions of the same logical event reuse this value under a new idempotency_key.'
        ),
    ]
    notification_type: Annotated[
        NotificationType,
        Field(
            description='Wholesale feed notification type discriminator. MUST match event.event_type.'
        ),
    ]
    fired_at: Annotated[
        AwareDatetime,
        Field(
            description='ISO 8601 timestamp when the seller initiated this webhook fire. Distinct from event.created_at, which is when the seller observed or recorded the feed change.'
        ),
    ]
    subscriber_id: Annotated[
        str,
        Field(
            description='Identifies which notification_configs[] entry is receiving this fire. Echoed from the registered subscriber_id.',
            max_length=64,
            min_length=1,
            pattern='^[A-Za-z0-9_.:-]{1,64}$',
        ),
    ]
    account_id: Annotated[
        str,
        Field(
            description='Seller account identifier for the account scope that registered this webhook through sync_accounts.accounts[].notification_configs[]. Required because wholesale feed webhooks are account-anchored notifications.'
        ),
    ]
    wholesale_feed_version: Annotated[
        str,
        Field(
            description='Opaque post-change version token for the affected wholesale feed. Store it only after applying the event. A stale mirror repairs with its last applied version; uncertain or bulk repair omits the conditional token.'
        ),
    ]
    product_payload_view: Annotated[
        ProductPayloadView | None,
        Field(
            description='Product representation selected by the receiving notification config. Present on product.* fires; canonical uses canonical_product/canonical_pricing_options and legacy uses product/pricing_options.'
        ),
    ] = None
    previous_wholesale_feed_version: Annotated[
        str | None,
        Field(
            description='Opaque version token for the affected wholesale feed before this change, when the seller can cheaply provide it. Receivers MAY use this to detect obvious gaps, but MUST NOT require it.'
        ),
    ] = None
    cache_scope: Annotated[
        CacheScope,
        Field(
            description='Cache layer affected by this change. MUST equal event.payload.applies_to.scope. Mirrors the cache_scope returned by list_products / get_signals for the affected wholesale feed.'
        ),
    ]
    event: Annotated[
        wholesale_feed_event.WholesaleFeedEvent,
        Field(
            description='The actual product, signal, or bulk-change event. Consumers MAY apply this payload to their local mirror. Before any binding action, or when ordering/gap checks fail, consumers MUST reconcile through list_products / get_signals.'
        ),
    ]
    ext: ext_1.ExtensionObject | None = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var account_id : str
var cache_scope : adcp.types.generated_poc.core.wholesale_feed_webhook.CacheScope
var event : adcp.types.generated_poc.core.wholesale_feed_event.WholesaleFeedEvent
var ext : adcp.types.generated_poc.core.ext.ExtensionObject | None
var fired_at : pydantic.types.AwareDatetime
var idempotency_key : str
var model_config
var notification_id : uuid.UUID
var notification_type : adcp.types.generated_poc.core.wholesale_feed_webhook.NotificationType
var previous_wholesale_feed_version : str | None
var product_payload_view : adcp.types.generated_poc.core.wholesale_feed_webhook.ProductPayloadView | None
var subscriber_id : str
var wholesale_feed_version : str

Inherited members

class ZipAsset (**data: Any)
Expand source code
class ZipAsset(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    asset_type: Annotated[
        Literal['zip'],
        Field(
            description='Discriminator identifying this as a zip-bundled asset. See /schemas/creative/asset-types for the registry.'
        ),
    ] = 'zip'
    url: Annotated[AnyUrl, Field(description='URL where the zip archive is hosted. Must be HTTPS.')]
    max_file_size_kb: Annotated[
        int | None,
        Field(
            description='Maximum file size in kilobytes. Receivers should reject zips exceeding this.',
            ge=0,
        ),
    ] = None
    entry_point: Annotated[
        str | None,
        Field(
            description="Relative path to the entry file within the zip (typically 'index.html'). Receivers default to 'index.html' if absent."
        ),
    ] = None
    allowed_inner_extensions: Annotated[
        list[str] | None,
        Field(
            description="File extensions permitted inside the zip (e.g., ['html', 'css', 'js', 'png', 'jpg', 'svg', 'webp', 'json', 'woff2']). Receivers may reject zips containing other extensions."
        ),
    ] = None
    backup_image_url: Annotated[
        AnyUrl | None,
        Field(
            description='Fallback image URL for environments that cannot render the bundled creative (e.g., non-HTML5 endpoints, ad blockers). Recommended for HTML5 banners.'
        ),
    ] = None
    digest: Annotated[
        str | None,
        Field(
            description='Optional SHA-256 content digest of the zip archive (sha256:<hex>) for integrity verification. Lets receivers detect tampered or stale archives.',
            pattern='^sha256:[a-f0-9]{64}$',
        ),
    ] = None
    accessibility: Annotated[
        Accessibility | None,
        Field(description='Self-declared accessibility properties for this opaque creative'),
    ] = None
    provenance: Annotated[
        provenance_1.Provenance | None,
        Field(
            description='Provenance metadata for this asset, overrides manifest-level provenance'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var accessibility : adcp.types.generated_poc.core.assets.zip_asset.Accessibility | None
var allowed_inner_extensions : list[str] | None
var asset_type : Literal['zip']
var backup_image_url : pydantic.networks.AnyUrl | None
var digest : str | None
var entry_point : str | None
var max_file_size_kb : int | None
var model_config
var provenance : adcp.types.generated_poc.core.provenance.Provenance | None
var url : pydantic.networks.AnyUrl

Inherited members