Module adcp.decisioning.specialisms

Per-specialism Protocol classes.

Adopters claim specialisms via DecisioningCapabilities.specialisms and implement the matching Protocol's methods on their :class:DecisioningPlatform subclass. Method names are unified across specialisms — a platform claiming both sales-non-guaranteed and sales-broadcast-tv implements create_media_buy once and returns a hybrid :class:SalesResult that branches per call.

Public surface re-exported from :mod:adcp.decisioning.specialisms:

  • :class:SalesPlatform — covers the spec sales-* slugs (non-guaranteed, guaranteed, broadcast-tv, social, proposal-mode, catalog-driven) under one unified hybrid shape.
  • :class:SignalsPlatform — covers signal-marketplace. Two methods: get_signals (catalog discovery) and activate_signal (provisioning onto destination platforms).
  • :class:OwnedSignalsPlatform — covers signal-owned. One method: get_signals (publisher-owned signal catalog discovery; signals are already usable on seller inventory).
  • :class:AudiencePlatform — covers audience-sync. Two methods: sync_audiences (push first-party CRM audiences with delta upsert) and poll_audience_statuses (batch state read).
  • :class:CreativeBuilderPlatform — covers creative-template + creative-generative. Required build_creative; optional preview_creative, sync_creatives. Unified shape per JS commit 841616d7 (F13) — wire spec doesn't distinguish template-driven transform from brief-to-creative generation. (No separate refine_creative method — refinement is invoked via build_creative with creative_id referencing the prior build, per schemas/cache/media-buy/build-creative-request.json.)
  • :class:CreativeAdServerPlatform — covers creative-ad-server. Stateful library + per-creative pricing + tag generation. Required build_creative, preview_creative, list_creatives, get_creative_delivery; optional sync_creatives.
  • :class:CampaignGovernancePlatform — covers governance-spend-authority + governance-delivery-monitor. Required check_governance, sync_plans, report_plan_outcome, get_plan_audit_logs. NOTE: a third governance slug, governance-aware-seller, names a SELLER claim (sales-* archetype that composes with a governance agent) — it does NOT implement this Protocol; it integrates WITH a platform that does. That slug stays unenforced until sync_governance handler shim wiring lands for sales adopters.
  • :class:BrandRightsPlatform — covers brand-rights. Required get_brand_identity, get_rights, acquire_rights (multi-arm acquisition result), and update_rights (inline or handoff).
  • :class:ContentStandardsPlatform — covers content-standards. 6 required CRUD + calibration + validation methods; 2 optional analyzer reads (get_media_buy_artifacts, get_creative_features).
  • :class:PropertyListsPlatform — covers property-lists. Standard 5-method CRUD with fetch-token issuance.
  • :class:CollectionListsPlatform — covers collection-lists. Parallel CRUD shape on collection-list types.

The breadth sprint is now COMPLETE — every spec specialism slug except governance-aware-seller has REQUIRED_METHODS coverage and a Protocol class. governance-aware-seller stays unenforced by design (it's a SELLER composition claim, not a wire-implementor claim — see :class:CampaignGovernancePlatform docstring).

Sub-modules

adcp.decisioning.specialisms.audience

AudiencePlatform Protocol — covers the audience-sync specialism …

adcp.decisioning.specialisms.brand_rights

BrandRightsPlatform Protocol — covers brand-rights …

adcp.decisioning.specialisms.content_standards

ContentStandardsPlatform Protocol — covers content-standards …

adcp.decisioning.specialisms.creative

CreativeBuilderPlatform Protocol — covers creative-template + creative-generative …

adcp.decisioning.specialisms.creative_ad_server

CreativeAdServerPlatform Protocol — covers creative-ad-server …

adcp.decisioning.specialisms.governance

CampaignGovernancePlatform Protocol — covers governance-spend-authority and governance-delivery-monitor …

adcp.decisioning.specialisms.lists

PropertyListsPlatform + CollectionListsPlatform — list-publishing specialisms …

adcp.decisioning.specialisms.sales

SalesPlatform Protocol — covers every sales-* specialism …

adcp.decisioning.specialisms.signals

SignalsPlatform Protocols for marketplace and owned signals …

Classes

class AudiencePlatform (*args, **kwargs)
Expand source code
@runtime_checkable
class AudiencePlatform(Protocol, Generic[TMeta]):
    """Sync first-party CRM audiences with delta upsert semantics.

    Methods may be sync (return ``T`` directly) or async (return
    ``Awaitable[T]``); the dispatch adapter detects via
    :func:`asyncio.iscoroutinefunction` and runs sync methods on a
    thread pool.

    Throw :class:`adcp.decisioning.AdcpError` for buyer-fixable
    rejection (``AUDIENCE_TOO_SMALL``, ``REFERENCE_NOT_FOUND``, etc.);
    the framework projects to the wire structured-error envelope.
    """

    def sync_audiences(
        self,
        audiences: Sequence[SyncAudiencesAudience],
        ctx: RequestContext[TMeta],
    ) -> MaybeAsync[SyncAudiencesSuccessResponse]:
        """Push audiences to the platform.

        Framework handles batching, idempotency, and cross-tenant
        scoping; the adopter handles match-rate computation and
        activation lifecycle.

        Sync acknowledgment with status changes via
        ``ctx.publish_status_change``: return per-audience result rows
        immediately (``'pending'`` / ``'matching'`` are valid sync
        outcomes). The match-rate computation and activation pipeline
        run in the background — call
        ``ctx.publish_status_change(resource_type='audience', ...)``
        from the platform's webhook handler / job queue / cron when
        each audience reaches a terminal state.

        :param audiences: List of audience rows projected from the
            wire ``SyncAudiencesRequest.audiences[]`` field. Adopter
            ergonomic — receives the list directly rather than the
            full request.
        :raises adcp.decisioning.AdcpError: for buyer-fixable
            rejection (e.g., ``AUDIENCE_TOO_SMALL``).
        """
        ...

    def poll_audience_statuses(
        self,
        audience_ids: Sequence[str],
        ctx: RequestContext[TMeta],
    ) -> MaybeAsync[Mapping[str, str]]:
        """Batch-poll current status for one or more audiences.

        Sync — this is a state-read, not a mutating operation. Useful
        for buyer-side polling outside the framework's task envelope
        (e.g., querying long-lived audiences) and for adapter code
        that needs to check N audiences at once.

        Returns a ``dict[audience_id, AudienceStatus]``. Audiences not
        found are omitted from the map (callers handle missing keys);
        raise ``AdcpError(code='REFERENCE_NOT_FOUND')`` only when the
        entire batch is unresolvable for the tenant.

        Single-audience polling is
        ``poll_audience_statuses([id], ctx).get(id)``. The batch shape
        composes with upstream identity-graph APIs that natively
        return per-audience-id arrays — adopters do NOT need to wrap
        a single-id lookup over an N-call loop.

        Adopter-internal helper — not surfaced as a wire tool. Used
        by adopter code orchestrating cross-platform audience flows
        and by the framework's optional bulk-status middleware.
        """
        ...

Sync first-party CRM audiences with delta upsert semantics.

Methods may be sync (return T directly) or async (return Awaitable[T]); the dispatch adapter detects via :func:asyncio.iscoroutinefunction and runs sync methods on a thread pool.

Throw :class:AdcpError for buyer-fixable rejection (AUDIENCE_TOO_SMALL, REFERENCE_NOT_FOUND, etc.); the framework projects to the wire structured-error envelope.

Ancestors

  • typing.Protocol
  • typing.Generic

Methods

def poll_audience_statuses(self, audience_ids: Sequence[str], ctx: RequestContext[TMeta]) ‑> MaybeAsync[Mapping[str, str]]
Expand source code
def poll_audience_statuses(
    self,
    audience_ids: Sequence[str],
    ctx: RequestContext[TMeta],
) -> MaybeAsync[Mapping[str, str]]:
    """Batch-poll current status for one or more audiences.

    Sync — this is a state-read, not a mutating operation. Useful
    for buyer-side polling outside the framework's task envelope
    (e.g., querying long-lived audiences) and for adapter code
    that needs to check N audiences at once.

    Returns a ``dict[audience_id, AudienceStatus]``. Audiences not
    found are omitted from the map (callers handle missing keys);
    raise ``AdcpError(code='REFERENCE_NOT_FOUND')`` only when the
    entire batch is unresolvable for the tenant.

    Single-audience polling is
    ``poll_audience_statuses([id], ctx).get(id)``. The batch shape
    composes with upstream identity-graph APIs that natively
    return per-audience-id arrays — adopters do NOT need to wrap
    a single-id lookup over an N-call loop.

    Adopter-internal helper — not surfaced as a wire tool. Used
    by adopter code orchestrating cross-platform audience flows
    and by the framework's optional bulk-status middleware.
    """
    ...

Batch-poll current status for one or more audiences.

Sync — this is a state-read, not a mutating operation. Useful for buyer-side polling outside the framework's task envelope (e.g., querying long-lived audiences) and for adapter code that needs to check N audiences at once.

Returns a dict[audience_id, AudienceStatus]. Audiences not found are omitted from the map (callers handle missing keys); raise AdcpError(code='REFERENCE_NOT_FOUND') only when the entire batch is unresolvable for the tenant.

Single-audience polling is poll_audience_statuses([id], ctx).get(id). The batch shape composes with upstream identity-graph APIs that natively return per-audience-id arrays — adopters do NOT need to wrap a single-id lookup over an N-call loop.

Adopter-internal helper — not surfaced as a wire tool. Used by adopter code orchestrating cross-platform audience flows and by the framework's optional bulk-status middleware.

def sync_audiences(self, audiences: Sequence[SyncAudiencesAudience], ctx: RequestContext[TMeta]) ‑> MaybeAsync[SyncAudiencesSuccessResponse]
Expand source code
def sync_audiences(
    self,
    audiences: Sequence[SyncAudiencesAudience],
    ctx: RequestContext[TMeta],
) -> MaybeAsync[SyncAudiencesSuccessResponse]:
    """Push audiences to the platform.

    Framework handles batching, idempotency, and cross-tenant
    scoping; the adopter handles match-rate computation and
    activation lifecycle.

    Sync acknowledgment with status changes via
    ``ctx.publish_status_change``: return per-audience result rows
    immediately (``'pending'`` / ``'matching'`` are valid sync
    outcomes). The match-rate computation and activation pipeline
    run in the background — call
    ``ctx.publish_status_change(resource_type='audience', ...)``
    from the platform's webhook handler / job queue / cron when
    each audience reaches a terminal state.

    :param audiences: List of audience rows projected from the
        wire ``SyncAudiencesRequest.audiences[]`` field. Adopter
        ergonomic — receives the list directly rather than the
        full request.
    :raises adcp.decisioning.AdcpError: for buyer-fixable
        rejection (e.g., ``AUDIENCE_TOO_SMALL``).
    """
    ...

Push audiences to the platform.

Framework handles batching, idempotency, and cross-tenant scoping; the adopter handles match-rate computation and activation lifecycle.

Sync acknowledgment with status changes via ctx.publish_status_change: return per-audience result rows immediately ('pending' / 'matching' are valid sync outcomes). The match-rate computation and activation pipeline run in the background — call ctx.publish_status_change(resource_type='audience', ...) from the platform's webhook handler / job queue / cron when each audience reaches a terminal state.

:param audiences: List of audience rows projected from the wire SyncAudiencesRequest.audiences[] field. Adopter ergonomic — receives the list directly rather than the full request. :raises adcp.decisioning.AdcpError: for buyer-fixable rejection (e.g., AUDIENCE_TOO_SMALL).

class BrandRightsPlatform (*args, **kwargs)
Expand source code
@runtime_checkable
class BrandRightsPlatform(Protocol, Generic[TMeta]):
    """Brand identity discovery + rights licensing.

    Methods may be sync (return ``T`` directly) or async (return
    ``Awaitable[T]``); the dispatch adapter detects via
    :func:`asyncio.iscoroutinefunction` and runs sync methods on a
    thread pool.

    Throw :class:`adcp.decisioning.AdcpError` for buyer-fixable
    REQUEST rejection (``REFERENCE_NOT_FOUND``, ``INVALID_REQUEST``,
    ``BUDGET_TOO_LOW``). For spec-defined GRANT rejection (rights
    unavailable in jurisdiction, talent dispute pending) return the
    :class:`AcquireRightsRejectedResponse` arm so the buyer sees the
    structured wire response with ``reason`` + ``suggestions``.
    """

    def get_brand_identity(
        self,
        req: GetBrandIdentityRequest,
        ctx: RequestContext[TMeta],
    ) -> MaybeAsync[GetBrandIdentitySuccessResponse]:
        """Read brand identity record — ``brand_id``, ``house``,
        localized ``names``, optional logos / industries /
        ``keller_type``. Sync; no async ceremony.

        :raises adcp.decisioning.AdcpError: ``code='REFERENCE_NOT_FOUND'``
            when the brand reference doesn't resolve to an identity
            the platform tracks.
        """
        ...

    def get_rights(
        self,
        req: GetRightsRequest,
        ctx: RequestContext[TMeta],
    ) -> MaybeAsync[GetRightsSuccessResponse]:
        """List rights matching a brand + use query.

        Sync read; framework wraps the response in the wire envelope.
        Returning an empty ``rights`` array is valid (= "no rights
        available for the requested terms"); throw ``AdcpError`` only
        for buyer-fixable rejection (e.g., unsupported jurisdiction).

        Note: the wire field is ``rights``, NOT ``offerings``.
        Adopters who named their internal model ``offerings``
        translate at this seam.
        """
        ...

    def acquire_rights(
        self,
        req: AcquireRightsRequest,
        ctx: RequestContext[TMeta],
    ) -> MaybeAsync[AcquireRightsResponse]:
        """Acquire rights — buyer commits to an offering.

        Three discriminated wire arms:

        * :class:`AcquireRightsAcquiredResponse` — rights granted
          immediately. Carries ``rights_id``, ``status: 'acquired'``,
          ``brand_id``, ``terms``, ``generation_credentials``
          (scoped per-LLM-provider keys), and ``rights_constraint``
          so the buyer can plumb the grant directly into creative
          generation.
        * :class:`AcquireRightsPendingResponse` — clearance pending
          counter-signature, legal review, or rights-holder approval.
          Carries ``rights_id``, ``status: 'pending_approval'``,
          ``brand_id``, plus optional ``detail`` and
          ``estimated_response_time``. **Async delivery is
          webhook-only** — the buyer's ``push_notification_config.url``
          receives the eventual ``Acquired`` or ``Rejected`` outcome.
          The spec does NOT define a polling tool for
          ``acquire_rights``; do not reach for ``tasks_get`` here.
        * :class:`AcquireRightsRejectedResponse` — terminal rejection.
          Carries ``rights_id``, ``status: 'rejected'``, ``brand_id``,
          ``reason``, and optional ``suggestions[]`` for buyer
          remediation.

        Pre-flight (catalog availability, agency authorization) MUST
        run sync regardless of arm — invalid requests reject before
        allocating any state.

        :raises adcp.decisioning.AdcpError: only for buyer-fixable
            REQUEST rejection (``INVALID_REQUEST``,
            ``BUDGET_TOO_LOW``). For GRANT rejection return the
            :class:`AcquireRightsRejectedResponse` arm — that's the
            structured business outcome path.
        """
        ...

    def update_rights(
        self,
        req: UpdateRightsRequest,
        ctx: RequestContext[TMeta],
    ) -> MaybeAsync[UpdateRightsResponse]:
        """Update an existing rights grant.

        Return the update result inline. The beta.5 response schema has no
        Submitted arm, so task/workflow handoffs are rejected before launch.
        A buyer-supplied push configuration is reserved for later rights
        notifications, not a pollable task lifecycle for this response.
        """
        ...

    def verify_brand_claim(
        self,
        req: VerifyBrandClaimRequest,
        ctx: RequestContext[TMeta],
    ) -> MaybeAsync[VerifyBrandClaimResponse]:
        """Verify one brand claim.

        Optional beta 3 surface. Implement when the brand agent can
        authoritatively confirm or reject parent, subsidiary, property, or
        trademark claims.
        """
        ...

    def verify_brand_claims(
        self,
        req: VerifyBrandClaimsRequest,
        ctx: RequestContext[TMeta],
    ) -> MaybeAsync[VerifyBrandClaimsResponseBulk]:
        """Verify many brand claims in one call.

        Optional bulk companion to :meth:`verify_brand_claim`.
        """
        ...

Brand identity discovery + rights licensing.

Methods may be sync (return T directly) or async (return Awaitable[T]); the dispatch adapter detects via :func:asyncio.iscoroutinefunction and runs sync methods on a thread pool.

Throw :class:AdcpError for buyer-fixable REQUEST rejection (REFERENCE_NOT_FOUND, INVALID_REQUEST, BUDGET_TOO_LOW). For spec-defined GRANT rejection (rights unavailable in jurisdiction, talent dispute pending) return the :class:AcquireRightsRejectedResponse arm so the buyer sees the structured wire response with reason + suggestions.

Ancestors

  • typing.Protocol
  • typing.Generic

Methods

def acquire_rights(self, req: AcquireRightsRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[AcquireRightsResponse]
Expand source code
def acquire_rights(
    self,
    req: AcquireRightsRequest,
    ctx: RequestContext[TMeta],
) -> MaybeAsync[AcquireRightsResponse]:
    """Acquire rights — buyer commits to an offering.

    Three discriminated wire arms:

    * :class:`AcquireRightsAcquiredResponse` — rights granted
      immediately. Carries ``rights_id``, ``status: 'acquired'``,
      ``brand_id``, ``terms``, ``generation_credentials``
      (scoped per-LLM-provider keys), and ``rights_constraint``
      so the buyer can plumb the grant directly into creative
      generation.
    * :class:`AcquireRightsPendingResponse` — clearance pending
      counter-signature, legal review, or rights-holder approval.
      Carries ``rights_id``, ``status: 'pending_approval'``,
      ``brand_id``, plus optional ``detail`` and
      ``estimated_response_time``. **Async delivery is
      webhook-only** — the buyer's ``push_notification_config.url``
      receives the eventual ``Acquired`` or ``Rejected`` outcome.
      The spec does NOT define a polling tool for
      ``acquire_rights``; do not reach for ``tasks_get`` here.
    * :class:`AcquireRightsRejectedResponse` — terminal rejection.
      Carries ``rights_id``, ``status: 'rejected'``, ``brand_id``,
      ``reason``, and optional ``suggestions[]`` for buyer
      remediation.

    Pre-flight (catalog availability, agency authorization) MUST
    run sync regardless of arm — invalid requests reject before
    allocating any state.

    :raises adcp.decisioning.AdcpError: only for buyer-fixable
        REQUEST rejection (``INVALID_REQUEST``,
        ``BUDGET_TOO_LOW``). For GRANT rejection return the
        :class:`AcquireRightsRejectedResponse` arm — that's the
        structured business outcome path.
    """
    ...

Acquire rights — buyer commits to an offering.

Three discriminated wire arms:

  • :class:AcquireRightsAcquiredResponse — rights granted immediately. Carries rights_id, status: 'acquired', brand_id, terms, generation_credentials (scoped per-LLM-provider keys), and rights_constraint so the buyer can plumb the grant directly into creative generation.
  • :class:AcquireRightsPendingResponse — clearance pending counter-signature, legal review, or rights-holder approval. Carries rights_id, status: 'pending_approval', brand_id, plus optional detail and estimated_response_time. Async delivery is webhook-only — the buyer's push_notification_config.url receives the eventual Acquired or Rejected outcome. The spec does NOT define a polling tool for acquire_rights; do not reach for tasks_get here.
  • :class:AcquireRightsRejectedResponse — terminal rejection. Carries rights_id, status: 'rejected', brand_id, reason, and optional suggestions[] for buyer remediation.

Pre-flight (catalog availability, agency authorization) MUST run sync regardless of arm — invalid requests reject before allocating any state.

:raises adcp.decisioning.AdcpError: only for buyer-fixable REQUEST rejection (INVALID_REQUEST, BUDGET_TOO_LOW). For GRANT rejection return the :class:AcquireRightsRejectedResponse arm — that's the structured business outcome path.

def get_brand_identity(self, req: GetBrandIdentityRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[GetBrandIdentitySuccessResponse]
Expand source code
def get_brand_identity(
    self,
    req: GetBrandIdentityRequest,
    ctx: RequestContext[TMeta],
) -> MaybeAsync[GetBrandIdentitySuccessResponse]:
    """Read brand identity record — ``brand_id``, ``house``,
    localized ``names``, optional logos / industries /
    ``keller_type``. Sync; no async ceremony.

    :raises adcp.decisioning.AdcpError: ``code='REFERENCE_NOT_FOUND'``
        when the brand reference doesn't resolve to an identity
        the platform tracks.
    """
    ...

Read brand identity record — brand_id, house, localized names, optional logos / industries / keller_type. Sync; no async ceremony.

:raises adcp.decisioning.AdcpError: code='REFERENCE_NOT_FOUND' when the brand reference doesn't resolve to an identity the platform tracks.

def get_rights(self, req: GetRightsRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[GetRightsSuccessResponse]
Expand source code
def get_rights(
    self,
    req: GetRightsRequest,
    ctx: RequestContext[TMeta],
) -> MaybeAsync[GetRightsSuccessResponse]:
    """List rights matching a brand + use query.

    Sync read; framework wraps the response in the wire envelope.
    Returning an empty ``rights`` array is valid (= "no rights
    available for the requested terms"); throw ``AdcpError`` only
    for buyer-fixable rejection (e.g., unsupported jurisdiction).

    Note: the wire field is ``rights``, NOT ``offerings``.
    Adopters who named their internal model ``offerings``
    translate at this seam.
    """
    ...

List rights matching a brand + use query.

Sync read; framework wraps the response in the wire envelope. Returning an empty rights array is valid (= "no rights available for the requested terms"); throw AdcpError only for buyer-fixable rejection (e.g., unsupported jurisdiction).

Note: the wire field is rights, NOT offerings. Adopters who named their internal model offerings translate at this seam.

def update_rights(self, req: UpdateRightsRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[UpdateRightsResponse]
Expand source code
def update_rights(
    self,
    req: UpdateRightsRequest,
    ctx: RequestContext[TMeta],
) -> MaybeAsync[UpdateRightsResponse]:
    """Update an existing rights grant.

    Return the update result inline. The beta.5 response schema has no
    Submitted arm, so task/workflow handoffs are rejected before launch.
    A buyer-supplied push configuration is reserved for later rights
    notifications, not a pollable task lifecycle for this response.
    """
    ...

Update an existing rights grant.

Return the update result inline. The beta.5 response schema has no Submitted arm, so task/workflow handoffs are rejected before launch. A buyer-supplied push configuration is reserved for later rights notifications, not a pollable task lifecycle for this response.

def verify_brand_claim(self, req: VerifyBrandClaimRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[VerifyBrandClaimResponse]
Expand source code
def verify_brand_claim(
    self,
    req: VerifyBrandClaimRequest,
    ctx: RequestContext[TMeta],
) -> MaybeAsync[VerifyBrandClaimResponse]:
    """Verify one brand claim.

    Optional beta 3 surface. Implement when the brand agent can
    authoritatively confirm or reject parent, subsidiary, property, or
    trademark claims.
    """
    ...

Verify one brand claim.

Optional beta 3 surface. Implement when the brand agent can authoritatively confirm or reject parent, subsidiary, property, or trademark claims.

def verify_brand_claims(self, req: VerifyBrandClaimsRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[VerifyBrandClaimsResponseBulk]
Expand source code
def verify_brand_claims(
    self,
    req: VerifyBrandClaimsRequest,
    ctx: RequestContext[TMeta],
) -> MaybeAsync[VerifyBrandClaimsResponseBulk]:
    """Verify many brand claims in one call.

    Optional bulk companion to :meth:`verify_brand_claim`.
    """
    ...

Verify many brand claims in one call.

Optional bulk companion to :meth:verify_brand_claim.

class CampaignGovernancePlatform (*args, **kwargs)
Expand source code
@runtime_checkable
class CampaignGovernancePlatform(Protocol, Generic[TMeta]):
    """Runtime governance decisioning for advertiser campaigns.

    A decision API: the agent inspects a proposed action (or running
    delivery) and returns ``approved``, ``denied``, or ``conditions``
    (approved-if). Status changes (plan moving from
    ``pending_approval`` → ``active`` → ``closed``) flow via
    ``ctx.publish_status_change(resource_type='plan', ...)``.

    Methods may be sync (return ``T`` directly) or async (return
    ``Awaitable[T]``); the dispatch adapter detects via
    :func:`asyncio.iscoroutinefunction` and runs sync methods on a
    thread pool.

    Throw :class:`adcp.decisioning.AdcpError` for buyer-fixable
    rejection (``PLAN_NOT_FOUND``, ``INVALID_REQUEST``, etc.). Use
    the :meth:`check_governance` response ``status: 'denied'`` for
    governance decisions that ARE the answer (the plan exists and
    the agent is rejecting the action) — that's a legitimate
    business outcome, not an error.
    """

    def check_governance(
        self,
        req: CheckGovernanceRequest,
        ctx: RequestContext[TMeta],
    ) -> MaybeAsync[CheckGovernanceResponse]:
        """Runtime governance decision.

        A buyer sends an intent-shaped request (``target_agent`` plus the
        exact downstream ``tool`` and ``payload``); an executing service sends
        an execution-shaped request (opaque ``governance_context`` plus
        seller-authoritative ``planned_delivery``). Modern responses identify
        the shape with ``check_type``.

        Conditions are valid only for ``check_type='intent'``. Execution is a
        binary approved/denied boundary. Its ``phase`` is ``purchase``,
        ``modification``, or ``delivery`` and never exposes the buyer's plan or
        original payload to the service.

        :raises adcp.decisioning.AdcpError: for buyer-fixable
            rejection (``PLAN_NOT_FOUND``, ``INVALID_REQUEST``).
            ``status: 'denied'`` on the response is the
            governance-decision-as-answer path — not an error.
        """
        ...

    def sync_plans(
        self,
        req: SyncPlansRequest,
        ctx: RequestContext[TMeta],
    ) -> MaybeAsync[SyncPlansResponse]:
        """Plan CRUD with delta upsert semantics.

        Buyers sync their campaign plans into the governance agent so
        the agent can maintain spend authority + delivery context.
        The agent tracks plan state across the campaign lifecycle
        (pending_approval → active → closed); transitions are emitted
        via ``ctx.publish_status_change(resource_type='plan', ...)``.
        """
        ...

    def report_plan_outcome(
        self,
        req: ReportPlanOutcomeRequest,
        ctx: RequestContext[TMeta],
    ) -> MaybeAsync[ReportPlanOutcomeResponse]:
        """Buyer/orchestrator outcome reporting after a seller response.

        Completed and failed reports settle the exact approved action;
        buyer-attributed delivery observations reconcile a prior seller
        delivery check. Sellers themselves report canonical delivery through
        ``check_governance(phase='delivery')``.
        """
        ...

    def get_plan_audit_logs(
        self,
        req: GetPlanAuditLogsRequest,
        ctx: RequestContext[TMeta],
    ) -> MaybeAsync[GetPlanAuditLogsResponse]:
        """Audit log read.

        Returns the chronological history of governance decisions +
        outcome reports for a plan. Buyers and operators use this to
        reconstruct who approved what + when, what conditions were
        attached, and what the seller reported.
        """
        ...

Runtime governance decisioning for advertiser campaigns.

A decision API: the agent inspects a proposed action (or running delivery) and returns approved, denied, or conditions (approved-if). Status changes (plan moving from pending_approval → active → closed) flow via ctx.publish_status_change(resource_type='plan', ...).

Methods may be sync (return T directly) or async (return Awaitable[T]); the dispatch adapter detects via :func:asyncio.iscoroutinefunction and runs sync methods on a thread pool.

Throw :class:AdcpError for buyer-fixable rejection (PLAN_NOT_FOUND, INVALID_REQUEST, etc.). Use the :meth:check_governance response status: 'denied' for governance decisions that ARE the answer (the plan exists and the agent is rejecting the action) — that's a legitimate business outcome, not an error.

Ancestors

  • typing.Protocol
  • typing.Generic

Methods

def check_governance(self, req: CheckGovernanceRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[CheckGovernanceResponse]
Expand source code
def check_governance(
    self,
    req: CheckGovernanceRequest,
    ctx: RequestContext[TMeta],
) -> MaybeAsync[CheckGovernanceResponse]:
    """Runtime governance decision.

    A buyer sends an intent-shaped request (``target_agent`` plus the
    exact downstream ``tool`` and ``payload``); an executing service sends
    an execution-shaped request (opaque ``governance_context`` plus
    seller-authoritative ``planned_delivery``). Modern responses identify
    the shape with ``check_type``.

    Conditions are valid only for ``check_type='intent'``. Execution is a
    binary approved/denied boundary. Its ``phase`` is ``purchase``,
    ``modification``, or ``delivery`` and never exposes the buyer's plan or
    original payload to the service.

    :raises adcp.decisioning.AdcpError: for buyer-fixable
        rejection (``PLAN_NOT_FOUND``, ``INVALID_REQUEST``).
        ``status: 'denied'`` on the response is the
        governance-decision-as-answer path — not an error.
    """
    ...

Runtime governance decision.

A buyer sends an intent-shaped request (target_agent plus the exact downstream tool and payload); an executing service sends an execution-shaped request (opaque governance_context plus seller-authoritative planned_delivery). Modern responses identify the shape with check_type.

Conditions are valid only for check_type='intent'. Execution is a binary approved/denied boundary. Its phase is purchase, modification, or delivery and never exposes the buyer's plan or original payload to the service.

:raises adcp.decisioning.AdcpError: for buyer-fixable rejection (PLAN_NOT_FOUND, INVALID_REQUEST). status: 'denied' on the response is the governance-decision-as-answer path — not an error.

def get_plan_audit_logs(self, req: GetPlanAuditLogsRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[GetPlanAuditLogsResponse]
Expand source code
def get_plan_audit_logs(
    self,
    req: GetPlanAuditLogsRequest,
    ctx: RequestContext[TMeta],
) -> MaybeAsync[GetPlanAuditLogsResponse]:
    """Audit log read.

    Returns the chronological history of governance decisions +
    outcome reports for a plan. Buyers and operators use this to
    reconstruct who approved what + when, what conditions were
    attached, and what the seller reported.
    """
    ...

Audit log read.

Returns the chronological history of governance decisions + outcome reports for a plan. Buyers and operators use this to reconstruct who approved what + when, what conditions were attached, and what the seller reported.

def report_plan_outcome(self, req: ReportPlanOutcomeRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[ReportPlanOutcomeResponse]
Expand source code
def report_plan_outcome(
    self,
    req: ReportPlanOutcomeRequest,
    ctx: RequestContext[TMeta],
) -> MaybeAsync[ReportPlanOutcomeResponse]:
    """Buyer/orchestrator outcome reporting after a seller response.

    Completed and failed reports settle the exact approved action;
    buyer-attributed delivery observations reconcile a prior seller
    delivery check. Sellers themselves report canonical delivery through
    ``check_governance(phase='delivery')``.
    """
    ...

Buyer/orchestrator outcome reporting after a seller response.

Completed and failed reports settle the exact approved action; buyer-attributed delivery observations reconcile a prior seller delivery check. Sellers themselves report canonical delivery through check_governance(phase='delivery').

def sync_plans(self, req: SyncPlansRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[SyncPlansResponse]
Expand source code
def sync_plans(
    self,
    req: SyncPlansRequest,
    ctx: RequestContext[TMeta],
) -> MaybeAsync[SyncPlansResponse]:
    """Plan CRUD with delta upsert semantics.

    Buyers sync their campaign plans into the governance agent so
    the agent can maintain spend authority + delivery context.
    The agent tracks plan state across the campaign lifecycle
    (pending_approval → active → closed); transitions are emitted
    via ``ctx.publish_status_change(resource_type='plan', ...)``.
    """
    ...

Plan CRUD with delta upsert semantics.

Buyers sync their campaign plans into the governance agent so the agent can maintain spend authority + delivery context. The agent tracks plan state across the campaign lifecycle (pending_approval → active → closed); transitions are emitted via ctx.publish_status_change(resource_type='plan', ...).

class CollectionListsPlatform (*args, **kwargs)
Expand source code
@runtime_checkable
class CollectionListsPlatform(Protocol, Generic[TMeta]):
    """Collection-list CRUD with fetch-token issuance semantics.

    Parallel shape to :class:`PropertyListsPlatform`; covers
    program-level brand-safety lists (shows, series, podcasts) keyed
    by IMDb / Gracenote / EIDR ids.

    Same security model: ``create_*`` issues a per-seller
    ``fetch_token``, ``delete_*`` revokes it; compromise-driven
    revocation MUST trigger delete.
    """

    def create_collection_list(
        self,
        req: CreateCollectionListRequest,
        ctx: RequestContext[TMeta],
    ) -> MaybeAsync[CreateCollectionListResponse]:
        """Create a collection list. Returns a per-seller-scoped
        ``fetch_token``."""
        ...

    def update_collection_list(
        self,
        req: UpdateCollectionListRequest,
        ctx: RequestContext[TMeta],
    ) -> MaybeAsync[UpdateCollectionListResponse]:
        """Patch an existing collection list."""
        ...

    def get_collection_list(
        self,
        req: GetCollectionListRequest,
        ctx: RequestContext[TMeta],
    ) -> MaybeAsync[GetCollectionListResponse]:
        """Read a collection list by id."""
        ...

    def list_collection_lists(
        self,
        req: ListCollectionListsRequest,
        ctx: RequestContext[TMeta],
    ) -> MaybeAsync[ListCollectionListsResponse]:
        """Discover collection lists the caller is authorized to read."""
        ...

    def delete_collection_list(
        self,
        req: DeleteCollectionListRequest,
        ctx: RequestContext[TMeta],
    ) -> MaybeAsync[DeleteCollectionListResponse]:
        """Delete a collection list. Revokes the ``fetch_token`` and
        signals cache invalidation."""
        ...

Collection-list CRUD with fetch-token issuance semantics.

Parallel shape to :class:PropertyListsPlatform; covers program-level brand-safety lists (shows, series, podcasts) keyed by IMDb / Gracenote / EIDR ids.

Same security model: create_* issues a per-seller fetch_token, delete_* revokes it; compromise-driven revocation MUST trigger delete.

Ancestors

  • typing.Protocol
  • typing.Generic

Methods

def create_collection_list(self, req: CreateCollectionListRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[CreateCollectionListResponse]
Expand source code
def create_collection_list(
    self,
    req: CreateCollectionListRequest,
    ctx: RequestContext[TMeta],
) -> MaybeAsync[CreateCollectionListResponse]:
    """Create a collection list. Returns a per-seller-scoped
    ``fetch_token``."""
    ...

Create a collection list. Returns a per-seller-scoped fetch_token.

def delete_collection_list(self, req: DeleteCollectionListRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[DeleteCollectionListResponse]
Expand source code
def delete_collection_list(
    self,
    req: DeleteCollectionListRequest,
    ctx: RequestContext[TMeta],
) -> MaybeAsync[DeleteCollectionListResponse]:
    """Delete a collection list. Revokes the ``fetch_token`` and
    signals cache invalidation."""
    ...

Delete a collection list. Revokes the fetch_token and signals cache invalidation.

def get_collection_list(self, req: GetCollectionListRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[GetCollectionListResponse]
Expand source code
def get_collection_list(
    self,
    req: GetCollectionListRequest,
    ctx: RequestContext[TMeta],
) -> MaybeAsync[GetCollectionListResponse]:
    """Read a collection list by id."""
    ...

Read a collection list by id.

def list_collection_lists(self, req: ListCollectionListsRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[ListCollectionListsResponse]
Expand source code
def list_collection_lists(
    self,
    req: ListCollectionListsRequest,
    ctx: RequestContext[TMeta],
) -> MaybeAsync[ListCollectionListsResponse]:
    """Discover collection lists the caller is authorized to read."""
    ...

Discover collection lists the caller is authorized to read.

def update_collection_list(self, req: UpdateCollectionListRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[UpdateCollectionListResponse]
Expand source code
def update_collection_list(
    self,
    req: UpdateCollectionListRequest,
    ctx: RequestContext[TMeta],
) -> MaybeAsync[UpdateCollectionListResponse]:
    """Patch an existing collection list."""
    ...

Patch an existing collection list.

class CompactMediaBuyPlatform (*args, **kwargs)
Expand source code
@runtime_checkable
class CompactMediaBuyPlatform(Protocol, Generic[TMeta]):
    """Optional AdCP 3.2 compact lifecycle surface.

    Kept separate from :class:`SalesPlatform` so adding the 3.2 methods does
    not make legacy implementations fail runtime structural checks. Sellers
    declare the implemented subset through ``media_buy.lifecycle_tools``;
    boot validation checks each claimed method independently.
    """

    def list_products(
        self, req: ListProductsRequest, ctx: RequestContext[TMeta]
    ) -> MaybeAsync[ListProductsResponse]: ...

    def request_proposals(
        self, req: RequestProposalsRequest, ctx: RequestContext[TMeta]
    ) -> SalesResult[RequestProposalsResponse]: ...

    def refine_proposals(
        self, req: RefineProposalsRequest, ctx: RequestContext[TMeta]
    ) -> SalesResult[RefineProposalsResponse]: ...

    def decline_proposals(
        self, req: DeclineProposalsRequest, ctx: RequestContext[TMeta]
    ) -> SalesResult[DeclineProposalsResponse]: ...

    def buy_products(
        self, req: BuyProductsRequest, ctx: RequestContext[TMeta]
    ) -> SalesResult[BuyProductsResponse]: ...

    def accept_proposal(
        self, req: AcceptProposalRequest, ctx: RequestContext[TMeta]
    ) -> SalesResult[AcceptProposalResponse]: ...

    def control_media_buy(
        self, req: ControlMediaBuyRequest, ctx: RequestContext[TMeta]
    ) -> SalesResult[ControlMediaBuyResponse]: ...

Optional AdCP 3.2 compact lifecycle surface.

Kept separate from :class:SalesPlatform so adding the 3.2 methods does not make legacy implementations fail runtime structural checks. Sellers declare the implemented subset through media_buy.lifecycle_tools; boot validation checks each claimed method independently.

Ancestors

  • typing.Protocol
  • typing.Generic

Methods

def accept_proposal(self, req: AcceptProposalRequest, ctx: RequestContext[TMeta]) ‑> SalesResult[AcceptProposalResponse]
Expand source code
def accept_proposal(
    self, req: AcceptProposalRequest, ctx: RequestContext[TMeta]
) -> SalesResult[AcceptProposalResponse]: ...
def buy_products(self, req: BuyProductsRequest, ctx: RequestContext[TMeta]) ‑> SalesResult[BuyProductsResponse]
Expand source code
def buy_products(
    self, req: BuyProductsRequest, ctx: RequestContext[TMeta]
) -> SalesResult[BuyProductsResponse]: ...
def control_media_buy(self, req: ControlMediaBuyRequest, ctx: RequestContext[TMeta]) ‑> SalesResult[ControlMediaBuyResponse]
Expand source code
def control_media_buy(
    self, req: ControlMediaBuyRequest, ctx: RequestContext[TMeta]
) -> SalesResult[ControlMediaBuyResponse]: ...
def decline_proposals(self, req: DeclineProposalsRequest, ctx: RequestContext[TMeta]) ‑> SalesResult[DeclineProposalsResponse]
Expand source code
def decline_proposals(
    self, req: DeclineProposalsRequest, ctx: RequestContext[TMeta]
) -> SalesResult[DeclineProposalsResponse]: ...
def list_products(self, req: ListProductsRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[ListProductsResponse]
Expand source code
def list_products(
    self, req: ListProductsRequest, ctx: RequestContext[TMeta]
) -> MaybeAsync[ListProductsResponse]: ...
def refine_proposals(self, req: RefineProposalsRequest, ctx: RequestContext[TMeta]) ‑> SalesResult[RefineProposalsResponse]
Expand source code
def refine_proposals(
    self, req: RefineProposalsRequest, ctx: RequestContext[TMeta]
) -> SalesResult[RefineProposalsResponse]: ...
def request_proposals(self, req: RequestProposalsRequest, ctx: RequestContext[TMeta]) ‑> SalesResult[RequestProposalsResponse]
Expand source code
def request_proposals(
    self, req: RequestProposalsRequest, ctx: RequestContext[TMeta]
) -> SalesResult[RequestProposalsResponse]: ...
class ContentStandardsPlatform (*args, **kwargs)
Expand source code
@runtime_checkable
class ContentStandardsPlatform(Protocol, Generic[TMeta]):
    """Content standards CRUD + calibration + delivery validation.

    Methods may be sync (return ``T`` directly) or async (return
    ``Awaitable[T]``); the dispatch adapter detects via
    :func:`asyncio.iscoroutinefunction` and runs sync methods on a
    thread pool.

    Throw :class:`adcp.decisioning.AdcpError` for buyer-fixable
    rejection (``REFERENCE_NOT_FOUND``, ``INVALID_REQUEST``,
    ``POLICY_VIOLATION`` for buyer rights issues, etc.).
    """

    def list_content_standards(
        self,
        req: ListContentStandardsRequest,
        ctx: RequestContext[TMeta],
    ) -> MaybeAsync[ListContentStandardsResponse]:
        """Discover content standards published by this agent."""
        ...

    def get_content_standards(
        self,
        req: GetContentStandardsRequest,
        ctx: RequestContext[TMeta],
    ) -> MaybeAsync[GetContentStandardsResponse]:
        """Read a single content standard by id."""
        ...

    def create_content_standards(
        self,
        req: CreateContentStandardsRequest,
        ctx: RequestContext[TMeta],
    ) -> MaybeAsync[CreateContentStandardsResponse]:
        """Create a new content standard.

        Adopter validates the policy schema and returns the persisted
        record. Idempotent on the buyer's ``idempotency_key``.
        """
        ...

    def update_content_standards(
        self,
        req: UpdateContentStandardsRequest,
        ctx: RequestContext[TMeta],
    ) -> MaybeAsync[UpdateContentStandardsResponse]:
        """Update an existing content standard."""
        ...

    def calibrate_content(
        self,
        req: CalibrateContentRequest,
        ctx: RequestContext[TMeta],
    ) -> MaybeAsync[CalibrateContentResponse]:
        """Calibrate content against the published standards.

        Returns the standard's current calibration profile + any
        flags raised against the submitted content.
        """
        ...

    def validate_content_delivery(
        self,
        req: ValidateContentDeliveryRequest,
        ctx: RequestContext[TMeta],
    ) -> MaybeAsync[ValidateContentDeliveryResponse]:
        """Validate that a delivered media-buy / creative meets the
        buyer's declared content-standards.

        Sellers call this post-flight to confirm adjacency and policy
        conformance before issuing a
        ``validate_content_delivery_artifact`` to a governance agent.
        """
        ...

    # ---- Optional analyzer reads ----

    def get_media_buy_artifacts(
        self,
        req: GetMediaBuyArtifactsRequest,
        ctx: RequestContext[TMeta],
    ) -> MaybeAsync[GetMediaBuyArtifactsResponse]:
        """Read content artifacts produced during a media buy's flight.

        Optional — adopters who don't expose artifact archival omit.
        Required by governance receivers running adjacency validation.
        """
        ...

    def get_creative_features(
        self,
        req: GetCreativeFeaturesRequest,
        ctx: RequestContext[TMeta],
    ) -> MaybeAsync[GetCreativeFeaturesResponse]:
        """Read per-creative analyzed features (object detection,
        scene classification, transcript) extracted during calibration.

        Optional — adopters without analyzer pipelines omit.
        """
        ...

Content standards CRUD + calibration + delivery validation.

Methods may be sync (return T directly) or async (return Awaitable[T]); the dispatch adapter detects via :func:asyncio.iscoroutinefunction and runs sync methods on a thread pool.

Throw :class:AdcpError for buyer-fixable rejection (REFERENCE_NOT_FOUND, INVALID_REQUEST, POLICY_VIOLATION for buyer rights issues, etc.).

Ancestors

  • typing.Protocol
  • typing.Generic

Methods

def calibrate_content(self, req: CalibrateContentRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[CalibrateContentResponse]
Expand source code
def calibrate_content(
    self,
    req: CalibrateContentRequest,
    ctx: RequestContext[TMeta],
) -> MaybeAsync[CalibrateContentResponse]:
    """Calibrate content against the published standards.

    Returns the standard's current calibration profile + any
    flags raised against the submitted content.
    """
    ...

Calibrate content against the published standards.

Returns the standard's current calibration profile + any flags raised against the submitted content.

def create_content_standards(self, req: CreateContentStandardsRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[CreateContentStandardsResponse]
Expand source code
def create_content_standards(
    self,
    req: CreateContentStandardsRequest,
    ctx: RequestContext[TMeta],
) -> MaybeAsync[CreateContentStandardsResponse]:
    """Create a new content standard.

    Adopter validates the policy schema and returns the persisted
    record. Idempotent on the buyer's ``idempotency_key``.
    """
    ...

Create a new content standard.

Adopter validates the policy schema and returns the persisted record. Idempotent on the buyer's idempotency_key.

def get_content_standards(self, req: GetContentStandardsRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[GetContentStandardsResponse]
Expand source code
def get_content_standards(
    self,
    req: GetContentStandardsRequest,
    ctx: RequestContext[TMeta],
) -> MaybeAsync[GetContentStandardsResponse]:
    """Read a single content standard by id."""
    ...

Read a single content standard by id.

def get_creative_features(self, req: GetCreativeFeaturesRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[GetCreativeFeaturesResponse]
Expand source code
def get_creative_features(
    self,
    req: GetCreativeFeaturesRequest,
    ctx: RequestContext[TMeta],
) -> MaybeAsync[GetCreativeFeaturesResponse]:
    """Read per-creative analyzed features (object detection,
    scene classification, transcript) extracted during calibration.

    Optional — adopters without analyzer pipelines omit.
    """
    ...

Read per-creative analyzed features (object detection, scene classification, transcript) extracted during calibration.

Optional — adopters without analyzer pipelines omit.

def get_media_buy_artifacts(self, req: GetMediaBuyArtifactsRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[GetMediaBuyArtifactsResponse]
Expand source code
def get_media_buy_artifacts(
    self,
    req: GetMediaBuyArtifactsRequest,
    ctx: RequestContext[TMeta],
) -> MaybeAsync[GetMediaBuyArtifactsResponse]:
    """Read content artifacts produced during a media buy's flight.

    Optional — adopters who don't expose artifact archival omit.
    Required by governance receivers running adjacency validation.
    """
    ...

Read content artifacts produced during a media buy's flight.

Optional — adopters who don't expose artifact archival omit. Required by governance receivers running adjacency validation.

def list_content_standards(self, req: ListContentStandardsRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[ListContentStandardsResponse]
Expand source code
def list_content_standards(
    self,
    req: ListContentStandardsRequest,
    ctx: RequestContext[TMeta],
) -> MaybeAsync[ListContentStandardsResponse]:
    """Discover content standards published by this agent."""
    ...

Discover content standards published by this agent.

def update_content_standards(self, req: UpdateContentStandardsRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[UpdateContentStandardsResponse]
Expand source code
def update_content_standards(
    self,
    req: UpdateContentStandardsRequest,
    ctx: RequestContext[TMeta],
) -> MaybeAsync[UpdateContentStandardsResponse]:
    """Update an existing content standard."""
    ...

Update an existing content standard.

def validate_content_delivery(self, req: ValidateContentDeliveryRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[ValidateContentDeliveryResponse]
Expand source code
def validate_content_delivery(
    self,
    req: ValidateContentDeliveryRequest,
    ctx: RequestContext[TMeta],
) -> MaybeAsync[ValidateContentDeliveryResponse]:
    """Validate that a delivered media-buy / creative meets the
    buyer's declared content-standards.

    Sellers call this post-flight to confirm adjacency and policy
    conformance before issuing a
    ``validate_content_delivery_artifact`` to a governance agent.
    """
    ...

Validate that a delivered media-buy / creative meets the buyer's declared content-standards.

Sellers call this post-flight to confirm adjacency and policy conformance before issuing a validate_content_delivery_artifact to a governance agent.

class CreativeAdServerPlatform (*args, **kwargs)
Expand source code
@runtime_checkable
class CreativeAdServerPlatform(Protocol, Generic[TMeta]):
    """Stateful creative library + per-creative pricing + tag generation.

    Methods may be sync (return ``T`` directly) or async (return
    ``Awaitable[T]``); the dispatch adapter detects via
    :func:`asyncio.iscoroutinefunction` and runs sync methods on a
    thread pool.

    Throw :class:`adcp.decisioning.AdcpError` for buyer-fixable
    rejection.
    """

    def build_creative_legacy(
        self,
        req: LegacyBuildCreativeRequest,
        ctx: RequestContext[TMeta],
    ) -> SalesResult[
        LegacyBuildCreativeResponse1
        | LegacyBuildCreativeResponse2
        | LegacyBuildCreativeResponse3
        | LegacyBuildCreativeResponse4
        | LegacyBuildCreativeResponse5
        | Sequence[CreativeManifest]
        | CreativeManifest
    ]:
        """Build / retrieve creative tags. Two invocation modes:

        * **Library lookup**: ``req.creative_id`` references an
          existing creative; return the manifest with tag fields
          populated (``vast_tag``, click trackers, etc.). When
          ``req.media_buy_id`` + ``req.package_id`` are also set,
          generate placement-specific tags with macro substitution
          baked in.
        * **Inline build**: ``req.creative_manifest`` is provided
          directly; transform / wrap it (similar to template archetype
          but with ad-server side effects: register the creative in
          the library, generate the tag, etc.).

        Beta.5 permits a submitted task arm for slow tag-generation
        pipelines; return either handoff marker when work cannot finish inline.

        Return shape: see :meth:`CreativeBuilderPlatform.build_creative`
        for the discriminated-arm rules — single
        :class:`CreativeManifest`, ``Sequence[CreativeManifest]`` for
        multi-format, or :class:`BuildCreativeSuccessResponse` envelope
        when you need ``sandbox``/``expires_at``/``preview``.
        """
        ...

    def preview_creative_legacy(
        self,
        req: LegacyPreviewCreativeRequest,
        ctx: RequestContext[TMeta],
    ) -> SalesResult[
        LegacyPreviewCreativeResponse1
        | LegacyPreviewCreativeResponse2
        | LegacyPreviewCreativeResponse3
    ]:
        """Preview-only variant — sandbox URL or inline HTML, expires.

        NOT optional for ad-server adopters (distinct from
        :class:`CreativeBuilderPlatform.preview_creative`, which is optional).
        A handoff is valid only when ``req.allow_async`` is true.
        """
        ...

    def list_creatives(
        self,
        req: ListCreativesRequest,
        ctx: RequestContext[TMeta],
    ) -> MaybeAsync[ListCreativesResponse]:
        """Read creatives from the library.

        Filters + pagination. When ``req.include_assignments``,
        include the buyer's package-assignment graph. When
        ``req.include_pricing``, include vendor pricing options on
        each creative.
        """
        ...

    def get_creative_delivery(
        self,
        req: GetCreativeDeliveryRequest,
        ctx: RequestContext[TMeta],
    ) -> MaybeAsync[GetCreativeDeliveryResponse]:
        """Per-creative delivery actuals (impressions, spend, pacing).

        Sync — report-running platforms with manual report cycles
        return the latest cached actuals and emit ``delivery_report``
        status changes via ``ctx.publish_status_change`` when fresh
        reports are available.
        """
        ...

    def sync_creatives(
        self,
        req: SyncCreativesRequest,
        ctx: RequestContext[TMeta],
    ) -> SalesResult[SyncCreativesSuccessResponse]:
        """Push creatives. Optional — present-or-absent.

        Return the typed :class:`SyncCreativesSuccessResponse` for the
        sync fast path OR ``ctx.handoff_to_task(fn)`` for HITL —
        brand-suitability, S&P review. ``action: 'created'`` for new
        entries, ``'updated'`` for replacements, ``'unchanged'`` when
        matching. Optional ``status: 'pending_review'`` for sync-arm
        rows awaiting manual review.

        Same wire request type as the sales-* and creative-builder
        archetypes use (``SyncCreativesRequest`` — shared spec shape).
        """
        ...

Stateful creative library + per-creative pricing + tag generation.

Methods may be sync (return T directly) or async (return Awaitable[T]); the dispatch adapter detects via :func:asyncio.iscoroutinefunction and runs sync methods on a thread pool.

Throw :class:AdcpError for buyer-fixable rejection.

Ancestors

  • typing.Protocol
  • typing.Generic

Methods

def build_creative_legacy(self, req: LegacyBuildCreativeRequest, ctx: RequestContext[TMeta]) ‑> SalesResult[LegacyBuildCreativeResponse1 | LegacyBuildCreativeResponse2 | LegacyBuildCreativeResponse3 | LegacyBuildCreativeResponse4 | LegacyBuildCreativeResponse5 | Sequence[CreativeManifest] | CreativeManifest]
Expand source code
def build_creative_legacy(
    self,
    req: LegacyBuildCreativeRequest,
    ctx: RequestContext[TMeta],
) -> SalesResult[
    LegacyBuildCreativeResponse1
    | LegacyBuildCreativeResponse2
    | LegacyBuildCreativeResponse3
    | LegacyBuildCreativeResponse4
    | LegacyBuildCreativeResponse5
    | Sequence[CreativeManifest]
    | CreativeManifest
]:
    """Build / retrieve creative tags. Two invocation modes:

    * **Library lookup**: ``req.creative_id`` references an
      existing creative; return the manifest with tag fields
      populated (``vast_tag``, click trackers, etc.). When
      ``req.media_buy_id`` + ``req.package_id`` are also set,
      generate placement-specific tags with macro substitution
      baked in.
    * **Inline build**: ``req.creative_manifest`` is provided
      directly; transform / wrap it (similar to template archetype
      but with ad-server side effects: register the creative in
      the library, generate the tag, etc.).

    Beta.5 permits a submitted task arm for slow tag-generation
    pipelines; return either handoff marker when work cannot finish inline.

    Return shape: see :meth:`CreativeBuilderPlatform.build_creative`
    for the discriminated-arm rules — single
    :class:`CreativeManifest`, ``Sequence[CreativeManifest]`` for
    multi-format, or :class:`BuildCreativeSuccessResponse` envelope
    when you need ``sandbox``/``expires_at``/``preview``.
    """
    ...

Build / retrieve creative tags. Two invocation modes:

  • Library lookup: req.creative_id references an existing creative; return the manifest with tag fields populated (vast_tag, click trackers, etc.). When req.media_buy_id + req.package_id are also set, generate placement-specific tags with macro substitution baked in.
  • Inline build: req.creative_manifest is provided directly; transform / wrap it (similar to template archetype but with ad-server side effects: register the creative in the library, generate the tag, etc.).

Beta.5 permits a submitted task arm for slow tag-generation pipelines; return either handoff marker when work cannot finish inline.

Return shape: see :meth:CreativeBuilderPlatform.build_creative for the discriminated-arm rules — single :class:CreativeManifest, Sequence[CreativeManifest] for multi-format, or :class:BuildCreativeSuccessResponse envelope when you need sandbox/expires_at/preview.

def get_creative_delivery(self, req: GetCreativeDeliveryRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[GetCreativeDeliveryResponse]
Expand source code
def get_creative_delivery(
    self,
    req: GetCreativeDeliveryRequest,
    ctx: RequestContext[TMeta],
) -> MaybeAsync[GetCreativeDeliveryResponse]:
    """Per-creative delivery actuals (impressions, spend, pacing).

    Sync — report-running platforms with manual report cycles
    return the latest cached actuals and emit ``delivery_report``
    status changes via ``ctx.publish_status_change`` when fresh
    reports are available.
    """
    ...

Per-creative delivery actuals (impressions, spend, pacing).

Sync — report-running platforms with manual report cycles return the latest cached actuals and emit delivery_report status changes via ctx.publish_status_change when fresh reports are available.

def list_creatives(self, req: ListCreativesRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[ListCreativesResponse]
Expand source code
def list_creatives(
    self,
    req: ListCreativesRequest,
    ctx: RequestContext[TMeta],
) -> MaybeAsync[ListCreativesResponse]:
    """Read creatives from the library.

    Filters + pagination. When ``req.include_assignments``,
    include the buyer's package-assignment graph. When
    ``req.include_pricing``, include vendor pricing options on
    each creative.
    """
    ...

Read creatives from the library.

Filters + pagination. When req.include_assignments, include the buyer's package-assignment graph. When req.include_pricing, include vendor pricing options on each creative.

def preview_creative_legacy(self, req: LegacyPreviewCreativeRequest, ctx: RequestContext[TMeta]) ‑> SalesResult[LegacyPreviewCreativeResponse1 | LegacyPreviewCreativeResponse2 | LegacyPreviewCreativeResponse3]
Expand source code
def preview_creative_legacy(
    self,
    req: LegacyPreviewCreativeRequest,
    ctx: RequestContext[TMeta],
) -> SalesResult[
    LegacyPreviewCreativeResponse1
    | LegacyPreviewCreativeResponse2
    | LegacyPreviewCreativeResponse3
]:
    """Preview-only variant — sandbox URL or inline HTML, expires.

    NOT optional for ad-server adopters (distinct from
    :class:`CreativeBuilderPlatform.preview_creative`, which is optional).
    A handoff is valid only when ``req.allow_async`` is true.
    """
    ...

Preview-only variant — sandbox URL or inline HTML, expires.

NOT optional for ad-server adopters (distinct from :class:CreativeBuilderPlatform.preview_creative, which is optional). A handoff is valid only when req.allow_async is true.

def sync_creatives(self, req: SyncCreativesRequest, ctx: RequestContext[TMeta]) ‑> SalesResult[SyncCreativesSuccessResponse]
Expand source code
def sync_creatives(
    self,
    req: SyncCreativesRequest,
    ctx: RequestContext[TMeta],
) -> SalesResult[SyncCreativesSuccessResponse]:
    """Push creatives. Optional — present-or-absent.

    Return the typed :class:`SyncCreativesSuccessResponse` for the
    sync fast path OR ``ctx.handoff_to_task(fn)`` for HITL —
    brand-suitability, S&P review. ``action: 'created'`` for new
    entries, ``'updated'`` for replacements, ``'unchanged'`` when
    matching. Optional ``status: 'pending_review'`` for sync-arm
    rows awaiting manual review.

    Same wire request type as the sales-* and creative-builder
    archetypes use (``SyncCreativesRequest`` — shared spec shape).
    """
    ...

Push creatives. Optional — present-or-absent.

Return the typed :class:SyncCreativesSuccessResponse for the sync fast path OR ctx.handoff_to_task(fn) for HITL — brand-suitability, S&P review. action: 'created' for new entries, 'updated' for replacements, 'unchanged' when matching. Optional status: 'pending_review' for sync-arm rows awaiting manual review.

Same wire request type as the sales-* and creative-builder archetypes use (SyncCreativesRequest — shared spec shape).

class CreativeBuilderPlatform (*args, **kwargs)
Expand source code
@runtime_checkable
class CreativeBuilderPlatform(Protocol, Generic[TMeta]):
    """Produces creatives — template-driven or brief-driven (generative).

    Methods may be sync (return ``T`` directly) or async (return
    ``Awaitable[T]``); the dispatch adapter detects via
    :func:`asyncio.iscoroutinefunction` and runs sync methods on a
    thread pool.

    Throw :class:`adcp.decisioning.AdcpError` for buyer-fixable
    rejection (``UNSUPPORTED_FEATURE`` for missing optionals,
    ``POLICY_VIOLATION`` for buyer rights issues, etc.); the framework
    projects to the wire structured-error envelope.
    """

    def build_creative_legacy(
        self,
        req: LegacyBuildCreativeRequest,
        ctx: RequestContext[TMeta],
    ) -> SalesResult[
        LegacyBuildCreativeResponse1
        | LegacyBuildCreativeResponse2
        | LegacyBuildCreativeResponse3
        | LegacyBuildCreativeResponse4
        | LegacyBuildCreativeResponse5
        | Sequence[CreativeManifest]
        | CreativeManifest
    ]:
        """Build the creative.

        Single method covers template-driven transform
        (``req.template_id`` + asset slots), brief-to-creative
        generation (``req.brief``), and any hybrid the platform
        supports — adopters route internally on ``req`` shape.

        Return shape is discriminated by the wire spec's Single vs
        Multi response arms:

        * **Single manifest, no metadata**: return a :class:`CreativeManifest`
          directly. Framework wraps as ``{creative_manifest: <manifest>}``.
          Use this for single-format requests (``target_format_id``)
          when you don't need to set ``sandbox`` / ``expires_at`` /
          ``preview``.
        * **Multi-format manifests, no metadata**: return a
          ``Sequence[CreativeManifest]``. Framework wraps as
          ``{creative_manifests: [...]}``. Use for multi-format
          requests (``target_format_ids``) when you don't need rich
          metadata.
        * **Fully-shaped envelope**: return any non-Submitted beta.5 build
          response arm, including error, multiplicity, or estimate responses.
          Framework passes the envelope through unchanged. Do not construct a
          Submitted response directly; return a handoff marker so the
          framework issues and persists the task identity.

        Adopters route on ``req.target_format_ids`` (multi) vs
        ``req.target_format_id`` (single) and return the matching arm.
        Returning the wrong arm shape is an adopter contract violation
        that surfaces as schema-validation failure on the wire response.

        :raises adcp.decisioning.AdcpError: ``code='POLICY_VIOLATION'``
            (buyer lacks rights to the requested template / brand
            inputs), ``code='INVALID_REQUEST'`` (missing or
            unrecognized template_id).
        """
        ...

    def preview_creative_legacy(
        self,
        req: LegacyPreviewCreativeRequest,
        ctx: RequestContext[TMeta],
    ) -> SalesResult[
        LegacyPreviewCreativeResponse1
        | LegacyPreviewCreativeResponse2
        | LegacyPreviewCreativeResponse3
    ]:
        """Preview-only variant — sandbox URL or inline HTML, expires.

        Optional — generative-only adopters that don't render preview ahead of
        generation can omit it. Return a handoff only when
        ``req.allow_async`` is true; otherwise return inline or reject.
        """
        ...

    def sync_creatives(
        self,
        req: SyncCreativesRequest,
        ctx: RequestContext[TMeta],
    ) -> SalesResult[SyncCreativesSuccessResponse]:
        """Sync review surface — present-or-absent.

        Stateless platforms typically auto-approve; adopters needing
        mandatory pre-persist review return
        ``ctx.handoff_to_task(fn)`` to defer to a background task.
        Unified hybrid shape — return the typed
        :class:`SyncCreativesSuccessResponse` for the sync fast path
        OR ``ctx.handoff_to_task(fn)`` for HITL.

        Same wire request type as the sales-* archetypes use
        (``SyncCreativesRequest`` — shared spec shape); the
        per-archetype handler shim narrows the discriminated payload
        when adopters care about archetype-specific fields.
        """
        ...

    def validate_input(
        self,
        req: ValidateInputRequest,
        ctx: RequestContext[TMeta],
    ) -> MaybeAsync[ValidateInputResponse]:
        """Validate buyer-provided creative inputs before build.

        Optional beta 3 preflight surface. Platforms that can predict format
        compatibility without rendering should implement it; otherwise the
        framework returns ``UNSUPPORTED_FEATURE`` when called.
        """
        ...

Produces creatives — template-driven or brief-driven (generative).

Methods may be sync (return T directly) or async (return Awaitable[T]); the dispatch adapter detects via :func:asyncio.iscoroutinefunction and runs sync methods on a thread pool.

Throw :class:AdcpError for buyer-fixable rejection (UNSUPPORTED_FEATURE for missing optionals, POLICY_VIOLATION for buyer rights issues, etc.); the framework projects to the wire structured-error envelope.

Ancestors

  • typing.Protocol
  • typing.Generic

Methods

def build_creative_legacy(self, req: LegacyBuildCreativeRequest, ctx: RequestContext[TMeta]) ‑> SalesResult[LegacyBuildCreativeResponse1 | LegacyBuildCreativeResponse2 | LegacyBuildCreativeResponse3 | LegacyBuildCreativeResponse4 | LegacyBuildCreativeResponse5 | Sequence[CreativeManifest] | CreativeManifest]
Expand source code
def build_creative_legacy(
    self,
    req: LegacyBuildCreativeRequest,
    ctx: RequestContext[TMeta],
) -> SalesResult[
    LegacyBuildCreativeResponse1
    | LegacyBuildCreativeResponse2
    | LegacyBuildCreativeResponse3
    | LegacyBuildCreativeResponse4
    | LegacyBuildCreativeResponse5
    | Sequence[CreativeManifest]
    | CreativeManifest
]:
    """Build the creative.

    Single method covers template-driven transform
    (``req.template_id`` + asset slots), brief-to-creative
    generation (``req.brief``), and any hybrid the platform
    supports — adopters route internally on ``req`` shape.

    Return shape is discriminated by the wire spec's Single vs
    Multi response arms:

    * **Single manifest, no metadata**: return a :class:`CreativeManifest`
      directly. Framework wraps as ``{creative_manifest: <manifest>}``.
      Use this for single-format requests (``target_format_id``)
      when you don't need to set ``sandbox`` / ``expires_at`` /
      ``preview``.
    * **Multi-format manifests, no metadata**: return a
      ``Sequence[CreativeManifest]``. Framework wraps as
      ``{creative_manifests: [...]}``. Use for multi-format
      requests (``target_format_ids``) when you don't need rich
      metadata.
    * **Fully-shaped envelope**: return any non-Submitted beta.5 build
      response arm, including error, multiplicity, or estimate responses.
      Framework passes the envelope through unchanged. Do not construct a
      Submitted response directly; return a handoff marker so the
      framework issues and persists the task identity.

    Adopters route on ``req.target_format_ids`` (multi) vs
    ``req.target_format_id`` (single) and return the matching arm.
    Returning the wrong arm shape is an adopter contract violation
    that surfaces as schema-validation failure on the wire response.

    :raises adcp.decisioning.AdcpError: ``code='POLICY_VIOLATION'``
        (buyer lacks rights to the requested template / brand
        inputs), ``code='INVALID_REQUEST'`` (missing or
        unrecognized template_id).
    """
    ...

Build the creative.

Single method covers template-driven transform (req.template_id + asset slots), brief-to-creative generation (req.brief), and any hybrid the platform supports — adopters route internally on req shape.

Return shape is discriminated by the wire spec's Single vs Multi response arms:

  • Single manifest, no metadata: return a :class:CreativeManifest directly. Framework wraps as {creative_manifest: <manifest>}. Use this for single-format requests (target_format_id) when you don't need to set sandbox / expires_at / preview.
  • Multi-format manifests, no metadata: return a Sequence[CreativeManifest]. Framework wraps as {creative_manifests: [...]}. Use for multi-format requests (target_format_ids) when you don't need rich metadata.
  • Fully-shaped envelope: return any non-Submitted beta.5 build response arm, including error, multiplicity, or estimate responses. Framework passes the envelope through unchanged. Do not construct a Submitted response directly; return a handoff marker so the framework issues and persists the task identity.

Adopters route on req.target_format_ids (multi) vs req.target_format_id (single) and return the matching arm. Returning the wrong arm shape is an adopter contract violation that surfaces as schema-validation failure on the wire response.

:raises adcp.decisioning.AdcpError: code='POLICY_VIOLATION' (buyer lacks rights to the requested template / brand inputs), code='INVALID_REQUEST' (missing or unrecognized template_id).

def preview_creative_legacy(self, req: LegacyPreviewCreativeRequest, ctx: RequestContext[TMeta]) ‑> SalesResult[LegacyPreviewCreativeResponse1 | LegacyPreviewCreativeResponse2 | LegacyPreviewCreativeResponse3]
Expand source code
def preview_creative_legacy(
    self,
    req: LegacyPreviewCreativeRequest,
    ctx: RequestContext[TMeta],
) -> SalesResult[
    LegacyPreviewCreativeResponse1
    | LegacyPreviewCreativeResponse2
    | LegacyPreviewCreativeResponse3
]:
    """Preview-only variant — sandbox URL or inline HTML, expires.

    Optional — generative-only adopters that don't render preview ahead of
    generation can omit it. Return a handoff only when
    ``req.allow_async`` is true; otherwise return inline or reject.
    """
    ...

Preview-only variant — sandbox URL or inline HTML, expires.

Optional — generative-only adopters that don't render preview ahead of generation can omit it. Return a handoff only when req.allow_async is true; otherwise return inline or reject.

def sync_creatives(self, req: SyncCreativesRequest, ctx: RequestContext[TMeta]) ‑> SalesResult[SyncCreativesSuccessResponse]
Expand source code
def sync_creatives(
    self,
    req: SyncCreativesRequest,
    ctx: RequestContext[TMeta],
) -> SalesResult[SyncCreativesSuccessResponse]:
    """Sync review surface — present-or-absent.

    Stateless platforms typically auto-approve; adopters needing
    mandatory pre-persist review return
    ``ctx.handoff_to_task(fn)`` to defer to a background task.
    Unified hybrid shape — return the typed
    :class:`SyncCreativesSuccessResponse` for the sync fast path
    OR ``ctx.handoff_to_task(fn)`` for HITL.

    Same wire request type as the sales-* archetypes use
    (``SyncCreativesRequest`` — shared spec shape); the
    per-archetype handler shim narrows the discriminated payload
    when adopters care about archetype-specific fields.
    """
    ...

Sync review surface — present-or-absent.

Stateless platforms typically auto-approve; adopters needing mandatory pre-persist review return ctx.handoff_to_task(fn) to defer to a background task. Unified hybrid shape — return the typed :class:SyncCreativesSuccessResponse for the sync fast path OR ctx.handoff_to_task(fn) for HITL.

Same wire request type as the sales-* archetypes use (SyncCreativesRequest — shared spec shape); the per-archetype handler shim narrows the discriminated payload when adopters care about archetype-specific fields.

def validate_input(self, req: ValidateInputRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[ValidateInputResponse]
Expand source code
def validate_input(
    self,
    req: ValidateInputRequest,
    ctx: RequestContext[TMeta],
) -> MaybeAsync[ValidateInputResponse]:
    """Validate buyer-provided creative inputs before build.

    Optional beta 3 preflight surface. Platforms that can predict format
    compatibility without rendering should implement it; otherwise the
    framework returns ``UNSUPPORTED_FEATURE`` when called.
    """
    ...

Validate buyer-provided creative inputs before build.

Optional beta 3 preflight surface. Platforms that can predict format compatibility without rendering should implement it; otherwise the framework returns UNSUPPORTED_FEATURE when called.

class OwnedSignalsPlatform (*args, **kwargs)
Expand source code
@runtime_checkable
class OwnedSignalsPlatform(Protocol, Generic[TMeta]):
    """Catalog discovery for seller-owned first-party signals.

    Methods may be sync (return ``T`` directly) or async (return
    ``Awaitable[T]``); the dispatch adapter detects via
    :func:`asyncio.iscoroutinefunction` and runs sync methods on a
    thread pool so a blocking sync handler doesn't serialize the event
    loop.

    Throw :class:`adcp.decisioning.AdcpError` for buyer-fixable
    rejection (``SIGNAL_NOT_FOUND``, ``POLICY_VIOLATION``,
    ``INVALID_REQUEST``, etc.); the framework projects to the wire
    structured-error envelope.
    """

    def get_signals(
        self,
        req: GetSignalsRequest,
        ctx: RequestContext[TMeta],
    ) -> DiscoveryResult[GetSignalsResponse]:
        """Catalog discovery — query your signal index, return signals
        matching the buyer's filters (industry, intent type, audience
        size, etc.).

        Return :class:`GetSignalsResponse` directly for the sync fast
        path. Brief-driven discovery MAY hand off via
        ``ctx.handoff_to_task(fn)`` when provider discovery needs
        background work (cross-provider fan-out, identity-graph lookups);
        the framework projects the handoff to the wire ``submitted``
        envelope. The buyer reaches the terminal
        :class:`GetSignalsResponse` via ``tasks/get`` polling, and — when
        the request carried ``push_notification_config`` — the framework
        also delivers the terminal completion / failure webhook to the
        buyer's URL from the background completion path (exactly once).

        Wholesale (``discovery_mode='wholesale'``) MUST return
        synchronously — a raw catalog read with no seller-side
        composition to background. Returning a handoff from a wholesale
        call is rejected at the framework layer with
        ``AdcpError(INVALID_REQUEST, field='discovery_mode')``.

        .. note::
            ``get_signals`` ships ONLY the ``submitted`` and ``working``
            async arms — it has **no** ``input_required`` arm (unlike
            ``get_products`` and ``create_media_buy``). Signal discovery
            cannot pause mid-task to solicit buyer clarification; the
            seller either completes the discovery or fails the task.

        :raises adcp.decisioning.AdcpError: ``code='POLICY_VIOLATION'``
            when the buyer doesn't have rights to the requested data
            category.
        """
        ...

Catalog discovery for seller-owned first-party signals.

Methods may be sync (return T directly) or async (return Awaitable[T]); the dispatch adapter detects via :func:asyncio.iscoroutinefunction and runs sync methods on a thread pool so a blocking sync handler doesn't serialize the event loop.

Throw :class:AdcpError for buyer-fixable rejection (SIGNAL_NOT_FOUND, POLICY_VIOLATION, INVALID_REQUEST, etc.); the framework projects to the wire structured-error envelope.

Ancestors

  • typing.Protocol
  • typing.Generic

Subclasses

Methods

def get_signals(self, req: GetSignalsRequest, ctx: RequestContext[TMeta]) ‑> DiscoveryResult[GetSignalsResponse]
Expand source code
def get_signals(
    self,
    req: GetSignalsRequest,
    ctx: RequestContext[TMeta],
) -> DiscoveryResult[GetSignalsResponse]:
    """Catalog discovery — query your signal index, return signals
    matching the buyer's filters (industry, intent type, audience
    size, etc.).

    Return :class:`GetSignalsResponse` directly for the sync fast
    path. Brief-driven discovery MAY hand off via
    ``ctx.handoff_to_task(fn)`` when provider discovery needs
    background work (cross-provider fan-out, identity-graph lookups);
    the framework projects the handoff to the wire ``submitted``
    envelope. The buyer reaches the terminal
    :class:`GetSignalsResponse` via ``tasks/get`` polling, and — when
    the request carried ``push_notification_config`` — the framework
    also delivers the terminal completion / failure webhook to the
    buyer's URL from the background completion path (exactly once).

    Wholesale (``discovery_mode='wholesale'``) MUST return
    synchronously — a raw catalog read with no seller-side
    composition to background. Returning a handoff from a wholesale
    call is rejected at the framework layer with
    ``AdcpError(INVALID_REQUEST, field='discovery_mode')``.

    .. note::
        ``get_signals`` ships ONLY the ``submitted`` and ``working``
        async arms — it has **no** ``input_required`` arm (unlike
        ``get_products`` and ``create_media_buy``). Signal discovery
        cannot pause mid-task to solicit buyer clarification; the
        seller either completes the discovery or fails the task.

    :raises adcp.decisioning.AdcpError: ``code='POLICY_VIOLATION'``
        when the buyer doesn't have rights to the requested data
        category.
    """
    ...

Catalog discovery — query your signal index, return signals matching the buyer's filters (industry, intent type, audience size, etc.).

Return :class:GetSignalsResponse directly for the sync fast path. Brief-driven discovery MAY hand off via ctx.handoff_to_task(fn) when provider discovery needs background work (cross-provider fan-out, identity-graph lookups); the framework projects the handoff to the wire submitted envelope. The buyer reaches the terminal :class:GetSignalsResponse via tasks/get polling, and — when the request carried push_notification_config — the framework also delivers the terminal completion / failure webhook to the buyer's URL from the background completion path (exactly once).

Wholesale (discovery_mode='wholesale') MUST return synchronously — a raw catalog read with no seller-side composition to background. Returning a handoff from a wholesale call is rejected at the framework layer with AdcpError(INVALID_REQUEST, field='discovery_mode').

Note

get_signals ships ONLY the submitted and working async arms — it has no input_required arm (unlike get_products and create_media_buy). Signal discovery cannot pause mid-task to solicit buyer clarification; the seller either completes the discovery or fails the task.

:raises adcp.decisioning.AdcpError: code='POLICY_VIOLATION' when the buyer doesn't have rights to the requested data category.

class PropertyListsPlatform (*args, **kwargs)
Expand source code
@runtime_checkable
class PropertyListsPlatform(Protocol, Generic[TMeta]):
    """Property-list CRUD with fetch-token issuance semantics.

    Methods may be sync (return ``T`` directly) or async (return
    ``Awaitable[T]``); the dispatch adapter detects via
    :func:`asyncio.iscoroutinefunction` and runs sync methods on a
    thread pool.

    Throw :class:`adcp.decisioning.AdcpError` for buyer-fixable
    rejection (``REFERENCE_NOT_FOUND``, ``POLICY_VIOLATION``).
    """

    def create_property_list(
        self,
        req: CreatePropertyListRequest,
        ctx: RequestContext[TMeta],
    ) -> MaybeAsync[CreatePropertyListResponse]:
        """Create a property list.

        Returns a ``fetch_token`` the buyer stores in their secret
        manager. Token is scoped to this ``list_id``; MUST NOT be
        reused across lists.
        """
        ...

    def update_property_list(
        self,
        req: UpdatePropertyListRequest,
        ctx: RequestContext[TMeta],
    ) -> MaybeAsync[UpdatePropertyListResponse]:
        """Patch an existing property list."""
        ...

    def get_property_list(
        self,
        req: GetPropertyListRequest,
        ctx: RequestContext[TMeta],
    ) -> MaybeAsync[GetPropertyListResponse]:
        """Read a property list by id.

        Sellers call this with the ``fetch_token`` from
        :meth:`create_property_list`.
        """
        ...

    def list_property_lists(
        self,
        req: ListPropertyListsRequest,
        ctx: RequestContext[TMeta],
    ) -> MaybeAsync[ListPropertyListsResponse]:
        """Discover property lists the caller is authorized to read."""
        ...

    def delete_property_list(
        self,
        req: DeletePropertyListRequest,
        ctx: RequestContext[TMeta],
    ) -> MaybeAsync[DeletePropertyListResponse]:
        """Delete a property list.

        MUST revoke the ``fetch_token`` immediately and signal cache
        invalidation to sellers (reduced ``cache_valid_until`` or a
        list-changed webhook). Compromise-driven revocation MUST
        also trigger this path.
        """
        ...

Property-list CRUD with fetch-token issuance semantics.

Methods may be sync (return T directly) or async (return Awaitable[T]); the dispatch adapter detects via :func:asyncio.iscoroutinefunction and runs sync methods on a thread pool.

Throw :class:AdcpError for buyer-fixable rejection (REFERENCE_NOT_FOUND, POLICY_VIOLATION).

Ancestors

  • typing.Protocol
  • typing.Generic

Methods

def create_property_list(self, req: CreatePropertyListRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[CreatePropertyListResponse]
Expand source code
def create_property_list(
    self,
    req: CreatePropertyListRequest,
    ctx: RequestContext[TMeta],
) -> MaybeAsync[CreatePropertyListResponse]:
    """Create a property list.

    Returns a ``fetch_token`` the buyer stores in their secret
    manager. Token is scoped to this ``list_id``; MUST NOT be
    reused across lists.
    """
    ...

Create a property list.

Returns a fetch_token the buyer stores in their secret manager. Token is scoped to this list_id; MUST NOT be reused across lists.

def delete_property_list(self, req: DeletePropertyListRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[DeletePropertyListResponse]
Expand source code
def delete_property_list(
    self,
    req: DeletePropertyListRequest,
    ctx: RequestContext[TMeta],
) -> MaybeAsync[DeletePropertyListResponse]:
    """Delete a property list.

    MUST revoke the ``fetch_token`` immediately and signal cache
    invalidation to sellers (reduced ``cache_valid_until`` or a
    list-changed webhook). Compromise-driven revocation MUST
    also trigger this path.
    """
    ...

Delete a property list.

MUST revoke the fetch_token immediately and signal cache invalidation to sellers (reduced cache_valid_until or a list-changed webhook). Compromise-driven revocation MUST also trigger this path.

def get_property_list(self, req: GetPropertyListRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[GetPropertyListResponse]
Expand source code
def get_property_list(
    self,
    req: GetPropertyListRequest,
    ctx: RequestContext[TMeta],
) -> MaybeAsync[GetPropertyListResponse]:
    """Read a property list by id.

    Sellers call this with the ``fetch_token`` from
    :meth:`create_property_list`.
    """
    ...

Read a property list by id.

Sellers call this with the fetch_token from :meth:create_property_list.

def list_property_lists(self, req: ListPropertyListsRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[ListPropertyListsResponse]
Expand source code
def list_property_lists(
    self,
    req: ListPropertyListsRequest,
    ctx: RequestContext[TMeta],
) -> MaybeAsync[ListPropertyListsResponse]:
    """Discover property lists the caller is authorized to read."""
    ...

Discover property lists the caller is authorized to read.

def update_property_list(self, req: UpdatePropertyListRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[UpdatePropertyListResponse]
Expand source code
def update_property_list(
    self,
    req: UpdatePropertyListRequest,
    ctx: RequestContext[TMeta],
) -> MaybeAsync[UpdatePropertyListResponse]:
    """Patch an existing property list."""
    ...

Patch an existing property list.

class SalesPlatform (*args, **kwargs)
Expand source code
@runtime_checkable
class SalesPlatform(Protocol, Generic[TMeta]):
    """Unified hybrid interface for every ``sales-*`` specialism.

    Methods may be sync (return ``T`` directly) or async (return
    ``Awaitable[T]``); the dispatch adapter detects via
    :func:`inspect.iscoroutinefunction` and runs sync methods on a
    thread pool via :func:`asyncio.to_thread` so a blocking sync
    handler doesn't serialize the event loop.

    Hybrid sellers (programmatic remnant + guaranteed inventory in
    one tenant) branch per call: return the Success directly for the
    sync fast path, return ``ctx.handoff_to_task(fn)`` for the HITL
    slow path. The framework dispatcher detects the
    :class:`TaskHandoff` via type-identity and projects to the wire
    ``Submitted`` envelope.

    Throw :class:`adcp.decisioning.AdcpError` for buyer-fixable
    rejection (``BUDGET_TOO_LOW``, ``POLICY_VIOLATION``, etc.); the
    framework projects to the wire structured-error envelope with
    code, recovery, field, suggestion, retry_after, details.
    """

    # ---- Required for every sales-* specialism ----

    def get_products(
        self,
        req: GetProductsRequest,
        ctx: RequestContext[TMeta],
    ) -> DiscoveryResult[GetProductsResponse]:
        """Catalog discovery — synchronous by default, MAY hand off.

        Return :class:`GetProductsResponse` directly for the sync fast
        path. Brief / refine discovery MAY hand off via
        ``ctx.handoff_to_task(fn)`` when composing the catalog needs
        background work (custom curation queue, slow proposal
        generation); the framework projects the handoff to the wire
        ``submitted`` envelope. The buyer reaches the terminal
        :class:`GetProductsResponse` via ``tasks/get`` polling, and — when
        the request carried ``push_notification_config`` — the framework
        also delivers the terminal completion / failure webhook to the
        buyer's URL from the background completion path (exactly once).
        ``get_products`` exposes the ``submitted`` / ``working`` /
        ``input_required`` async arms.

        Wholesale (``buying_mode='wholesale'``) MUST return synchronously
        — it is a raw rate-card read with no seller-side composition to
        background. A wholesale call that cannot finish within the
        buyer's ``time_budget`` declares the gap via ``incomplete[]`` on
        a sync response rather than handing off; returning a handoff from
        a wholesale call is rejected at the framework layer with
        ``AdcpError(INVALID_REQUEST, field='buying_mode')``.

        Brief-based proposal generation rides on a separate verb
        (``request_proposal``, adcp#3407); proposal-mode adopters
        surface the eventual products via
        ``ctx.publish_status_change(resource_type='proposal', ...)``
        rather than blocking ``get_products`` waiting for trafficker
        approval.

        **Buying mode dispatch:** when ``req.buying_mode == 'refine'`` and
        the platform implements :meth:`refine_get_products`, the framework
        dispatches there instead of this method.  Platforms that do not
        implement ``refine_get_products`` reject ``buying_mode='refine'``
        with ``AdcpError(INVALID_REQUEST, field='buying_mode')`` at the
        framework layer — the platform method is not called.
        """
        ...

    # Truly optional — adopters who don't implement refine_get_products
    # remain structurally conformant.  The framework uses
    # :func:`adcp.decisioning.has_refine_support` (a ``hasattr`` check) at
    # dispatch time and rejects ``buying_mode='refine'`` with
    # ``AdcpError(INVALID_REQUEST, field='buying_mode')`` when absent.
    #
    # Implementations match this signature::
    #
    #     def refine_get_products(
    #         self,
    #         req: GetProductsRequest,
    #         ctx: RequestContext[TMeta],
    #     ) -> MaybeAsync[RefineResult]: ...
    #
    # Return a :class:`adcp.decisioning.RefineResult` with ``products``,
    # ``proposals``, and exactly ``len(req.refine)`` outcomes in
    # ``per_refine_outcome``.  The framework constructs the wire
    # ``refinement_applied[]`` by zipping outcomes with ``req.refine`` —
    # adopters do NOT echo ``scope`` / ``product_id`` / ``proposal_id``
    # manually.

    def create_media_buy(
        self,
        req: CreateMediaBuyRequest,
        ctx: RequestContext[TMeta],
    ) -> SalesResult[CreateMediaBuySuccessResponse]:
        """Unified hybrid. Return :class:`CreateMediaBuySuccessResponse` directly
        for sync fast path; return :meth:`RequestContext.handoff_to_task`
        for HITL slow path.

        Pre-flight runs sync regardless of path so bad budgets reject
        before allocating a task id — call ``preflight()`` at the top,
        ``raise AdcpError(...)`` on rejection.

        Buyer pattern-matches on the response shape:

        * ``media_buy_id`` field present → sync success
        * ``task_id`` + ``status='submitted'`` → poll ``tasks_get`` or
          receive webhook

        **Framework injection:** when a :class:`~adcp.decisioning.ProductConfigStore`
        is wired via ``config_store=`` in
        :func:`adcp.decisioning.serve.create_adcp_server_from_platform`,
        the framework calls the store before invoking this method and
        injects the result as ``configs: dict[str, dict[str, Any]]``.
        Declare ``configs`` in your method signature to receive it::

            def create_media_buy(self, req, ctx, configs=None):
                configs = configs or {}
                line_item_id = configs.get(pkg.product_id, {}).get("line_item_id")

        When the store is not wired, or when ``req.packages`` is
        ``None`` (proposal-id flow), ``configs`` arrives as an empty
        dict ``{}``.
        """
        ...

    def update_media_buy(
        self,
        media_buy_id: str,
        patch: UpdateMediaBuyRequest,
        ctx: RequestContext[TMeta],
    ) -> MaybeAsync[UpdateMediaBuySuccessResponse]:
        """Mutate an in-flight media buy.

        v6.0 returns sync only — the per-tool response schema doesn't
        carry the ``Submitted`` arm yet (adcp#3392). Re-approval flows
        return the success with the ``status`` field omitted (in-spec
        per the schema description) and drive lifecycle via
        ``ctx.publish_status_change``. v6.1 + adcp#3392 expand this
        signature to :data:`SalesResult` so re-approval flows can
        hand off cleanly.
        """
        ...

    def sync_creatives(
        self,
        req: SyncCreativesRequest,
        ctx: RequestContext[TMeta],
    ) -> SalesResult[SyncCreativesSuccessResponse]:
        """Unified hybrid for creative review.

        Mixed approved/pending rows in a single sync response, OR
        hand off the whole batch to background standards-and-practices
        review. Adopters with pre-approved buyer pools fast-path; new
        buyers' creatives go to review.
        """
        ...

    def get_media_buy_delivery(
        self,
        req: GetMediaBuyDeliveryRequest,
        ctx: RequestContext[TMeta],
    ) -> MaybeAsync[GetMediaBuyDeliveryResponse]:
        """Sync delivery read — pacing, spend, impressions per package."""
        ...

    # ---- Optional (gated by specialism — present-or-absent) ----

    def get_media_buys(
        self,
        req: GetMediaBuysRequest,
        ctx: RequestContext[TMeta],
    ) -> MaybeAsync[GetMediaBuysResponse]:
        """List media buys for the resolved account.

        Required when claiming any ``sales-*`` specialism in v6.0 rc.1+.
        ``validate_platform`` fails server boot if a sales-claiming
        platform doesn't implement this.
        """
        ...

    def provide_performance_feedback(
        self,
        req: ProvidePerformanceFeedbackRequest,
        ctx: RequestContext[TMeta],
    ) -> MaybeAsync[ProvidePerformanceFeedbackResponse]:
        """Buyer-supplied performance signal back to the seller.

        Required when claiming any ``sales-*`` specialism in v6.0 rc.1+.
        """
        ...

    def list_creative_formats_legacy(
        self,
        req: ListCreativeFormatsRequest,
        ctx: RequestContext[TMeta],
    ) -> MaybeAsync[ListCreativeFormatsResponse]:
        """Catalog of accepted creative formats.

        Recommended for ``sales-*`` sellers that still serve pre-3.2
        versions. Deprecated in AdCP 3.2: publish purchasable canonical
        declarations through ``get_products`` / ``Product.format_options[]``.
        Sellers advertising only 3.2 or later can omit this method even
        with ``ADCP_DECISIONING_STRICT_VALIDATE_PLATFORM=1``.
        """
        ...

    def list_creatives(
        self,
        req: ListCreativesRequest,
        ctx: RequestContext[TMeta],
    ) -> MaybeAsync[ListCreativesResponse]:
        """List the seller's view of buyer-uploaded creatives.

        Required when claiming any ``sales-*`` specialism in v6.0 rc.1+.
        """
        ...

    # ---- Required when claiming ``sales-catalog-driven`` ----

    def sync_catalogs(
        self,
        req: SyncCatalogsRequest,
        ctx: RequestContext[TMeta],
    ) -> MaybeAsync[SyncCatalogsSuccessResponse]:
        """Sync product catalogs with the platform.

        **Required** when claiming ``sales-catalog-driven``.
        ``validate_platform`` hard-fails at server boot when this method is
        absent on a ``sales-catalog-driven`` platform.

        Discovery mode: when ``req.catalogs is None``, return the account's
        existing synced catalogs without modification (read-only path per
        AdCP spec). Check ``req.catalogs`` before applying any mutations::

            def sync_catalogs(self, req, ctx):
                if req.catalogs is None:
                    return self._get_existing_catalogs(ctx.account_id)
                return self._upsert_catalogs(req.catalogs, ctx)

        **Important:** ``req.delete_missing=True`` with ``req.catalogs=None``
        is spec-undefined — reject it with
        ``AdcpError("INVALID_REQUEST", field="catalogs")`` rather than
        silently deleting buyer-managed catalogs.

        Return a list of :class:`~adcp.types.SyncCatalogResult` rows
        (ergonomic form) or a fully-shaped
        :class:`~adcp.types.SyncCatalogsSuccessResponse`.
        """
        raise NotImplementedError

Unified hybrid interface for every sales-* specialism.

Methods may be sync (return T directly) or async (return Awaitable[T]); the dispatch adapter detects via :func:inspect.iscoroutinefunction and runs sync methods on a thread pool via :func:asyncio.to_thread so a blocking sync handler doesn't serialize the event loop.

Hybrid sellers (programmatic remnant + guaranteed inventory in one tenant) branch per call: return the Success directly for the sync fast path, return ctx.handoff_to_task(fn) for the HITL slow path. The framework dispatcher detects the :class:TaskHandoff via type-identity and projects to the wire Submitted envelope.

Throw :class:AdcpError for buyer-fixable rejection (BUDGET_TOO_LOW, POLICY_VIOLATION, etc.); the framework projects to the wire structured-error envelope with code, recovery, field, suggestion, retry_after, details.

Ancestors

  • typing.Protocol
  • typing.Generic

Methods

def create_media_buy(self, req: CreateMediaBuyRequest, ctx: RequestContext[TMeta]) ‑> SalesResult[CreateMediaBuySuccessResponse]
Expand source code
def create_media_buy(
    self,
    req: CreateMediaBuyRequest,
    ctx: RequestContext[TMeta],
) -> SalesResult[CreateMediaBuySuccessResponse]:
    """Unified hybrid. Return :class:`CreateMediaBuySuccessResponse` directly
    for sync fast path; return :meth:`RequestContext.handoff_to_task`
    for HITL slow path.

    Pre-flight runs sync regardless of path so bad budgets reject
    before allocating a task id — call ``preflight()`` at the top,
    ``raise AdcpError(...)`` on rejection.

    Buyer pattern-matches on the response shape:

    * ``media_buy_id`` field present → sync success
    * ``task_id`` + ``status='submitted'`` → poll ``tasks_get`` or
      receive webhook

    **Framework injection:** when a :class:`~adcp.decisioning.ProductConfigStore`
    is wired via ``config_store=`` in
    :func:`adcp.decisioning.serve.create_adcp_server_from_platform`,
    the framework calls the store before invoking this method and
    injects the result as ``configs: dict[str, dict[str, Any]]``.
    Declare ``configs`` in your method signature to receive it::

        def create_media_buy(self, req, ctx, configs=None):
            configs = configs or {}
            line_item_id = configs.get(pkg.product_id, {}).get("line_item_id")

    When the store is not wired, or when ``req.packages`` is
    ``None`` (proposal-id flow), ``configs`` arrives as an empty
    dict ``{}``.
    """
    ...

Unified hybrid. Return :class:CreateMediaBuySuccessResponse directly for sync fast path; return :meth:RequestContext.handoff_to_task for HITL slow path.

Pre-flight runs sync regardless of path so bad budgets reject before allocating a task id — call preflight() at the top, raise AdcpError(…) on rejection.

Buyer pattern-matches on the response shape:

  • media_buy_id field present → sync success
  • task_id + status='submitted' → poll tasks_get or receive webhook

Framework injection: when a :class:~adcp.decisioning.ProductConfigStore is wired via config_store= in :func:adcp.decisioning.serve.create_adcp_server_from_platform, the framework calls the store before invoking this method and injects the result as configs: dict[str, dict[str, Any]]. Declare configs in your method signature to receive it::

def create_media_buy(self, req, ctx, configs=None):
    configs = configs or {}
    line_item_id = configs.get(pkg.product_id, {}).get("line_item_id")

When the store is not wired, or when req.packages is None (proposal-id flow), configs arrives as an empty dict {}.

def get_media_buy_delivery(self, req: GetMediaBuyDeliveryRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[GetMediaBuyDeliveryResponse]
Expand source code
def get_media_buy_delivery(
    self,
    req: GetMediaBuyDeliveryRequest,
    ctx: RequestContext[TMeta],
) -> MaybeAsync[GetMediaBuyDeliveryResponse]:
    """Sync delivery read — pacing, spend, impressions per package."""
    ...

Sync delivery read — pacing, spend, impressions per package.

def get_media_buys(self, req: GetMediaBuysRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[GetMediaBuysResponse]
Expand source code
def get_media_buys(
    self,
    req: GetMediaBuysRequest,
    ctx: RequestContext[TMeta],
) -> MaybeAsync[GetMediaBuysResponse]:
    """List media buys for the resolved account.

    Required when claiming any ``sales-*`` specialism in v6.0 rc.1+.
    ``validate_platform`` fails server boot if a sales-claiming
    platform doesn't implement this.
    """
    ...

List media buys for the resolved account.

Required when claiming any sales-* specialism in v6.0 rc.1+. validate_platform fails server boot if a sales-claiming platform doesn't implement this.

def get_products(self, req: GetProductsRequest, ctx: RequestContext[TMeta]) ‑> DiscoveryResult[GetProductsResponse]
Expand source code
def get_products(
    self,
    req: GetProductsRequest,
    ctx: RequestContext[TMeta],
) -> DiscoveryResult[GetProductsResponse]:
    """Catalog discovery — synchronous by default, MAY hand off.

    Return :class:`GetProductsResponse` directly for the sync fast
    path. Brief / refine discovery MAY hand off via
    ``ctx.handoff_to_task(fn)`` when composing the catalog needs
    background work (custom curation queue, slow proposal
    generation); the framework projects the handoff to the wire
    ``submitted`` envelope. The buyer reaches the terminal
    :class:`GetProductsResponse` via ``tasks/get`` polling, and — when
    the request carried ``push_notification_config`` — the framework
    also delivers the terminal completion / failure webhook to the
    buyer's URL from the background completion path (exactly once).
    ``get_products`` exposes the ``submitted`` / ``working`` /
    ``input_required`` async arms.

    Wholesale (``buying_mode='wholesale'``) MUST return synchronously
    — it is a raw rate-card read with no seller-side composition to
    background. A wholesale call that cannot finish within the
    buyer's ``time_budget`` declares the gap via ``incomplete[]`` on
    a sync response rather than handing off; returning a handoff from
    a wholesale call is rejected at the framework layer with
    ``AdcpError(INVALID_REQUEST, field='buying_mode')``.

    Brief-based proposal generation rides on a separate verb
    (``request_proposal``, adcp#3407); proposal-mode adopters
    surface the eventual products via
    ``ctx.publish_status_change(resource_type='proposal', ...)``
    rather than blocking ``get_products`` waiting for trafficker
    approval.

    **Buying mode dispatch:** when ``req.buying_mode == 'refine'`` and
    the platform implements :meth:`refine_get_products`, the framework
    dispatches there instead of this method.  Platforms that do not
    implement ``refine_get_products`` reject ``buying_mode='refine'``
    with ``AdcpError(INVALID_REQUEST, field='buying_mode')`` at the
    framework layer — the platform method is not called.
    """
    ...

Catalog discovery — synchronous by default, MAY hand off.

Return :class:GetProductsResponse directly for the sync fast path. Brief / refine discovery MAY hand off via ctx.handoff_to_task(fn) when composing the catalog needs background work (custom curation queue, slow proposal generation); the framework projects the handoff to the wire submitted envelope. The buyer reaches the terminal :class:GetProductsResponse via tasks/get polling, and — when the request carried push_notification_config — the framework also delivers the terminal completion / failure webhook to the buyer's URL from the background completion path (exactly once). get_products exposes the submitted / working / input_required async arms.

Wholesale (buying_mode='wholesale') MUST return synchronously — it is a raw rate-card read with no seller-side composition to background. A wholesale call that cannot finish within the buyer's time_budget declares the gap via incomplete[] on a sync response rather than handing off; returning a handoff from a wholesale call is rejected at the framework layer with AdcpError(INVALID_REQUEST, field='buying_mode').

Brief-based proposal generation rides on a separate verb (request_proposal, adcp#3407); proposal-mode adopters surface the eventual products via ctx.publish_status_change(resource_type='proposal', ...) rather than blocking get_products waiting for trafficker approval.

Buying mode dispatch: when req.buying_mode == 'refine' and the platform implements :meth:refine_get_products, the framework dispatches there instead of this method. Platforms that do not implement refine_get_products reject buying_mode='refine' with AdcpError(INVALID_REQUEST, field='buying_mode') at the framework layer — the platform method is not called.

def list_creative_formats_legacy(self, req: ListCreativeFormatsRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[ListCreativeFormatsResponse]
Expand source code
def list_creative_formats_legacy(
    self,
    req: ListCreativeFormatsRequest,
    ctx: RequestContext[TMeta],
) -> MaybeAsync[ListCreativeFormatsResponse]:
    """Catalog of accepted creative formats.

    Recommended for ``sales-*`` sellers that still serve pre-3.2
    versions. Deprecated in AdCP 3.2: publish purchasable canonical
    declarations through ``get_products`` / ``Product.format_options[]``.
    Sellers advertising only 3.2 or later can omit this method even
    with ``ADCP_DECISIONING_STRICT_VALIDATE_PLATFORM=1``.
    """
    ...

Catalog of accepted creative formats.

Recommended for sales-* sellers that still serve pre-3.2 versions. Deprecated in AdCP 3.2: publish purchasable canonical declarations through get_products / Product.format_options[]. Sellers advertising only 3.2 or later can omit this method even with ADCP_DECISIONING_STRICT_VALIDATE_PLATFORM=1.

def list_creatives(self, req: ListCreativesRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[ListCreativesResponse]
Expand source code
def list_creatives(
    self,
    req: ListCreativesRequest,
    ctx: RequestContext[TMeta],
) -> MaybeAsync[ListCreativesResponse]:
    """List the seller's view of buyer-uploaded creatives.

    Required when claiming any ``sales-*`` specialism in v6.0 rc.1+.
    """
    ...

List the seller's view of buyer-uploaded creatives.

Required when claiming any sales-* specialism in v6.0 rc.1+.

def provide_performance_feedback(self, req: ProvidePerformanceFeedbackRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[ProvidePerformanceFeedbackResponse]
Expand source code
def provide_performance_feedback(
    self,
    req: ProvidePerformanceFeedbackRequest,
    ctx: RequestContext[TMeta],
) -> MaybeAsync[ProvidePerformanceFeedbackResponse]:
    """Buyer-supplied performance signal back to the seller.

    Required when claiming any ``sales-*`` specialism in v6.0 rc.1+.
    """
    ...

Buyer-supplied performance signal back to the seller.

Required when claiming any sales-* specialism in v6.0 rc.1+.

def sync_catalogs(self, req: SyncCatalogsRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[SyncCatalogsSuccessResponse]
Expand source code
def sync_catalogs(
    self,
    req: SyncCatalogsRequest,
    ctx: RequestContext[TMeta],
) -> MaybeAsync[SyncCatalogsSuccessResponse]:
    """Sync product catalogs with the platform.

    **Required** when claiming ``sales-catalog-driven``.
    ``validate_platform`` hard-fails at server boot when this method is
    absent on a ``sales-catalog-driven`` platform.

    Discovery mode: when ``req.catalogs is None``, return the account's
    existing synced catalogs without modification (read-only path per
    AdCP spec). Check ``req.catalogs`` before applying any mutations::

        def sync_catalogs(self, req, ctx):
            if req.catalogs is None:
                return self._get_existing_catalogs(ctx.account_id)
            return self._upsert_catalogs(req.catalogs, ctx)

    **Important:** ``req.delete_missing=True`` with ``req.catalogs=None``
    is spec-undefined — reject it with
    ``AdcpError("INVALID_REQUEST", field="catalogs")`` rather than
    silently deleting buyer-managed catalogs.

    Return a list of :class:`~adcp.types.SyncCatalogResult` rows
    (ergonomic form) or a fully-shaped
    :class:`~adcp.types.SyncCatalogsSuccessResponse`.
    """
    raise NotImplementedError

Sync product catalogs with the platform.

Required when claiming sales-catalog-driven. validate_platform hard-fails at server boot when this method is absent on a sales-catalog-driven platform.

Discovery mode: when req.catalogs is None, return the account's existing synced catalogs without modification (read-only path per AdCP spec). Check req.catalogs before applying any mutations::

def sync_catalogs(self, req, ctx):
    if req.catalogs is None:
        return self._get_existing_catalogs(ctx.account_id)
    return self._upsert_catalogs(req.catalogs, ctx)

Important: req.delete_missing=True with req.catalogs=None is spec-undefined — reject it with AdcpError("INVALID_REQUEST", field="catalogs") rather than silently deleting buyer-managed catalogs.

Return a list of :class:~adcp.types.SyncCatalogResult rows (ergonomic form) or a fully-shaped :class:~adcp.types.SyncCatalogsSuccessResponse.

def sync_creatives(self, req: SyncCreativesRequest, ctx: RequestContext[TMeta]) ‑> SalesResult[SyncCreativesSuccessResponse]
Expand source code
def sync_creatives(
    self,
    req: SyncCreativesRequest,
    ctx: RequestContext[TMeta],
) -> SalesResult[SyncCreativesSuccessResponse]:
    """Unified hybrid for creative review.

    Mixed approved/pending rows in a single sync response, OR
    hand off the whole batch to background standards-and-practices
    review. Adopters with pre-approved buyer pools fast-path; new
    buyers' creatives go to review.
    """
    ...

Unified hybrid for creative review.

Mixed approved/pending rows in a single sync response, OR hand off the whole batch to background standards-and-practices review. Adopters with pre-approved buyer pools fast-path; new buyers' creatives go to review.

def update_media_buy(self, media_buy_id: str, patch: UpdateMediaBuyRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[UpdateMediaBuySuccessResponse]
Expand source code
def update_media_buy(
    self,
    media_buy_id: str,
    patch: UpdateMediaBuyRequest,
    ctx: RequestContext[TMeta],
) -> MaybeAsync[UpdateMediaBuySuccessResponse]:
    """Mutate an in-flight media buy.

    v6.0 returns sync only — the per-tool response schema doesn't
    carry the ``Submitted`` arm yet (adcp#3392). Re-approval flows
    return the success with the ``status`` field omitted (in-spec
    per the schema description) and drive lifecycle via
    ``ctx.publish_status_change``. v6.1 + adcp#3392 expand this
    signature to :data:`SalesResult` so re-approval flows can
    hand off cleanly.
    """
    ...

Mutate an in-flight media buy.

v6.0 returns sync only — the per-tool response schema doesn't carry the Submitted arm yet (adcp#3392). Re-approval flows return the success with the status field omitted (in-spec per the schema description) and drive lifecycle via ctx.publish_status_change. v6.1 + adcp#3392 expand this signature to :data:SalesResult so re-approval flows can hand off cleanly.

class SignalsPlatform (*args, **kwargs)
Expand source code
@runtime_checkable
class SignalsPlatform(OwnedSignalsPlatform[TMeta], Protocol, Generic[TMeta]):
    """Catalog discovery + activation for marketplace/provisioned signals.

    Use this Protocol for ``signal-marketplace``. Use
    :class:`OwnedSignalsPlatform` for ``signal-owned`` platforms where
    returned signals are already usable in later media-buy targeting and
    there is no buyer-triggered destination provisioning step.
    """

    def activate_signal(
        self,
        req: ActivateSignalRequest,
        ctx: RequestContext[TMeta],
    ) -> MaybeAsync[ActivateSignalSuccessResponse]:
        """Provision a signal onto one or more destination platforms
        (Snap, Meta, TikTok, etc.).

        Returns the success-arm shape immediately with ``deployments``
        rows in their current state — ``'pending'`` is a valid sync
        return for slow activation pipelines.

        Subsequent state changes (per-deployment ``activating`` /
        ``deployed`` / ``failed``) flow via
        ``ctx.publish_status_change(resource_type='signal',
        resource_id=signal_agent_segment_id, payload=...)`` as each
        destination's identity-graph match completes.

        Use ``req.action='deactivate'`` for GDPR/CCPA-compliant
        teardown when campaigns end.

        :raises adcp.decisioning.AdcpError: ``code='SIGNAL_NOT_FOUND'``
            (unknown ``signal_agent_segment_id``),
            ``code='POLICY_VIOLATION'`` (buyer lacks rights to activate
            this data), or ``code='INVALID_REQUEST'`` (missing or
            unrecognized destination).
        """
        ...

Catalog discovery + activation for marketplace/provisioned signals.

Use this Protocol for signal-marketplace. Use :class:OwnedSignalsPlatform for signal-owned platforms where returned signals are already usable in later media-buy targeting and there is no buyer-triggered destination provisioning step.

Ancestors

Methods

def activate_signal(self, req: ActivateSignalRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[ActivateSignalSuccessResponse]
Expand source code
def activate_signal(
    self,
    req: ActivateSignalRequest,
    ctx: RequestContext[TMeta],
) -> MaybeAsync[ActivateSignalSuccessResponse]:
    """Provision a signal onto one or more destination platforms
    (Snap, Meta, TikTok, etc.).

    Returns the success-arm shape immediately with ``deployments``
    rows in their current state — ``'pending'`` is a valid sync
    return for slow activation pipelines.

    Subsequent state changes (per-deployment ``activating`` /
    ``deployed`` / ``failed``) flow via
    ``ctx.publish_status_change(resource_type='signal',
    resource_id=signal_agent_segment_id, payload=...)`` as each
    destination's identity-graph match completes.

    Use ``req.action='deactivate'`` for GDPR/CCPA-compliant
    teardown when campaigns end.

    :raises adcp.decisioning.AdcpError: ``code='SIGNAL_NOT_FOUND'``
        (unknown ``signal_agent_segment_id``),
        ``code='POLICY_VIOLATION'`` (buyer lacks rights to activate
        this data), or ``code='INVALID_REQUEST'`` (missing or
        unrecognized destination).
    """
    ...

Provision a signal onto one or more destination platforms (Snap, Meta, TikTok, etc.).

Returns the success-arm shape immediately with deployments rows in their current state — 'pending' is a valid sync return for slow activation pipelines.

Subsequent state changes (per-deployment activating / deployed / failed) flow via ctx.publish_status_change(resource_type='signal', resource_id=signal_agent_segment_id, payload=...) as each destination's identity-graph match completes.

Use req.action='deactivate' for GDPR/CCPA-compliant teardown when campaigns end.

:raises adcp.decisioning.AdcpError: code='SIGNAL_NOT_FOUND' (unknown signal_agent_segment_id), code='POLICY_VIOLATION' (buyer lacks rights to activate this data), or code='INVALID_REQUEST' (missing or unrecognized destination).

Inherited members