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 specsales-*slugs (non-guaranteed, guaranteed, broadcast-tv, social, proposal-mode, catalog-driven) under one unified hybrid shape. - :class:
SignalsPlatform— coverssignal-marketplace. Two methods:get_signals(catalog discovery) andactivate_signal(provisioning onto destination platforms). - :class:
OwnedSignalsPlatform— coverssignal-owned. One method:get_signals(publisher-owned signal catalog discovery; signals are already usable on seller inventory). - :class:
AudiencePlatform— coversaudience-sync. Two methods:sync_audiences(push first-party CRM audiences with delta upsert) andpoll_audience_statuses(batch state read). - :class:
CreativeBuilderPlatform— coverscreative-template+creative-generative. Requiredbuild_creative; optionalpreview_creative,sync_creatives. Unified shape per JS commit841616d7(F13) — wire spec doesn't distinguish template-driven transform from brief-to-creative generation. (No separaterefine_creativemethod — refinement is invoked viabuild_creativewithcreative_idreferencing the prior build, perschemas/cache/media-buy/build-creative-request.json.) - :class:
CreativeAdServerPlatform— coverscreative-ad-server. Stateful library + per-creative pricing + tag generation. Requiredbuild_creative,preview_creative,list_creatives,get_creative_delivery; optionalsync_creatives. - :class:
CampaignGovernancePlatform— coversgovernance-spend-authority+governance-delivery-monitor. Requiredcheck_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— coversbrand-rights. Requiredget_brand_identity,get_rights,acquire_rights(3-arm discriminated success union: acquired / pending / rejected). - :class:
ContentStandardsPlatform— coverscontent-standards. 6 required CRUD + calibration + validation methods; 2 optional analyzer reads (get_media_buy_artifacts,get_creative_features). - :class:
PropertyListsPlatform— coversproperty-lists. Standard 5-method CRUD with fetch-token issuance. - :class:
CollectionListsPlatform— coverscollection-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-syncspecialism … 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-authorityandgovernance-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
Tdirectly) or async (returnAwaitable[T]); the dispatch adapter detects via :func:asyncio.iscoroutinefunctionand runs sync methods on a thread pool.Throw :class:
AdcpErrorfor 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); raiseAdcpError(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 — callctx.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[ AcquireRightsAcquiredResponse | AcquireRightsPendingResponse | AcquireRightsRejectedResponse ]: """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 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
Tdirectly) or async (returnAwaitable[T]); the dispatch adapter detects via :func:asyncio.iscoroutinefunctionand runs sync methods on a thread pool.Throw :class:
AdcpErrorfor 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:AcquireRightsRejectedResponsearm so the buyer sees the structured wire response withreason+suggestions.Ancestors
- typing.Protocol
- typing.Generic
Methods
def acquire_rights(self, req: AcquireRightsRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[AcquireRightsAcquiredResponse | AcquireRightsPendingResponse | AcquireRightsRejectedResponse]-
Expand source code
def acquire_rights( self, req: AcquireRightsRequest, ctx: RequestContext[TMeta], ) -> MaybeAsync[ AcquireRightsAcquiredResponse | AcquireRightsPendingResponse | AcquireRightsRejectedResponse ]: """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. Carriesrights_id,status: 'acquired',brand_id,terms,generation_credentials(scoped per-LLM-provider keys), andrights_constraintso the buyer can plumb the grant directly into creative generation. - :class:
AcquireRightsPendingResponse— clearance pending counter-signature, legal review, or rights-holder approval. Carriesrights_id,status: 'pending_approval',brand_id, plus optionaldetailandestimated_response_time. Async delivery is webhook-only — the buyer'spush_notification_config.urlreceives the eventualAcquiredorRejectedoutcome. The spec does NOT define a polling tool foracquire_rights; do not reach fortasks_gethere. - :class:
AcquireRightsRejectedResponse— terminal rejection. Carriesrights_id,status: 'rejected',brand_id,reason, and optionalsuggestions[]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:AcquireRightsRejectedResponsearm — that's the structured business outcome path. - :class:
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, localizednames, 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
rightsarray is valid (= "no rights available for the requested terms"); throwAdcpErroronly for buyer-fixable rejection (e.g., unsupported jurisdiction).Note: the wire field is
rights, NOTofferings. Adopters who named their internal modelofferingstranslate at this seam. 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. Buyer (or seller, on the seller's behalf) sends a proposed action; the agent inspects it against the plan and returns approved / denied / conditions. The ``phase`` field discriminates the context: * ``'intent'`` — pre-action; agent decides whether the proposed action is permitted at all. * ``'delivery'`` — running campaign with actuals; agent decides whether to allow further spend / new packages. * ``'reconciliation'`` — post-flight; agent confirms the campaign's outcome matches what was approved. The agent's logic varies by phase. :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]: """Outcome reporting from sellers. Sellers report what actually happened (impressions delivered, spend incurred, status transitions) so the agent can calibrate future decisions. Typically called at terminal plan states or at agreed reconciliation cadences. """ ... 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, orconditions(approved-if). Status changes (plan moving frompending_approval→active→closed) flow viactx.publish_status_change(resource_type='plan', ...).Methods may be sync (return
Tdirectly) or async (returnAwaitable[T]); the dispatch adapter detects via :func:asyncio.iscoroutinefunctionand runs sync methods on a thread pool.Throw :class:
AdcpErrorfor buyer-fixable rejection (PLAN_NOT_FOUND,INVALID_REQUEST, etc.). Use the :meth:check_governanceresponsestatus: '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. Buyer (or seller, on the seller's behalf) sends a proposed action; the agent inspects it against the plan and returns approved / denied / conditions. The ``phase`` field discriminates the context: * ``'intent'`` — pre-action; agent decides whether the proposed action is permitted at all. * ``'delivery'`` — running campaign with actuals; agent decides whether to allow further spend / new packages. * ``'reconciliation'`` — post-flight; agent confirms the campaign's outcome matches what was approved. The agent's logic varies by phase. :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.
Buyer (or seller, on the seller's behalf) sends a proposed action; the agent inspects it against the plan and returns approved / denied / conditions.
The
phasefield discriminates the context:'intent'— pre-action; agent decides whether the proposed action is permitted at all.'delivery'— running campaign with actuals; agent decides whether to allow further spend / new packages.'reconciliation'— post-flight; agent confirms the campaign's outcome matches what was approved.
The agent's logic varies by phase.
: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]: """Outcome reporting from sellers. Sellers report what actually happened (impressions delivered, spend incurred, status transitions) so the agent can calibrate future decisions. Typically called at terminal plan states or at agreed reconciliation cadences. """ ...Outcome reporting from sellers.
Sellers report what actually happened (impressions delivered, spend incurred, status transitions) so the agent can calibrate future decisions. Typically called at terminal plan states or at agreed reconciliation cadences.
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-sellerfetch_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_tokenand 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 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
Tdirectly) or async (returnAwaitable[T]); the dispatch adapter detects via :func:asyncio.iscoroutinefunctionand runs sync methods on a thread pool.Throw :class:
AdcpErrorfor buyer-fixable rejection (REFERENCE_NOT_FOUND,INVALID_REQUEST,POLICY_VIOLATIONfor 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_artifactto 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( self, req: BuildCreativeRequest, ctx: RequestContext[TMeta], ) -> MaybeAsync[BuildCreativeSuccessResponse | 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.). Sync at the wire level — the per-tool ``build-creative-response.json`` ``oneOf`` doesn't include a ``Submitted`` arm (spec inconsistency tracked as ``adcontextprotocol/adcp#3392``). Until the spec rolls Submitted into the ``oneOf``, slow tag-generation pipelines await in-request; status changes flow via ``ctx.publish_status_change``. 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( self, req: PreviewCreativeRequest, ctx: RequestContext[TMeta], ) -> MaybeAsync[PreviewCreativeResponse]: """Preview-only variant — sandbox URL or inline HTML, expires. Always sync. NOT optional for ad-server adopters (distinct from :class:`CreativeBuilderPlatform.preview_creative`, which is optional) — buyers expect preview surface from any stateful creative library. """ ... 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
Tdirectly) or async (returnAwaitable[T]); the dispatch adapter detects via :func:asyncio.iscoroutinefunctionand runs sync methods on a thread pool.Throw :class:
AdcpErrorfor buyer-fixable rejection.Ancestors
- typing.Protocol
- typing.Generic
Methods
def build_creative(self, req: BuildCreativeRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[BuildCreativeSuccessResponse | Sequence[CreativeManifest] | CreativeManifest]-
Expand source code
def build_creative( self, req: BuildCreativeRequest, ctx: RequestContext[TMeta], ) -> MaybeAsync[BuildCreativeSuccessResponse | 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.). Sync at the wire level — the per-tool ``build-creative-response.json`` ``oneOf`` doesn't include a ``Submitted`` arm (spec inconsistency tracked as ``adcontextprotocol/adcp#3392``). Until the spec rolls Submitted into the ``oneOf``, slow tag-generation pipelines await in-request; status changes flow via ``ctx.publish_status_change``. 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_idreferences an existing creative; return the manifest with tag fields populated (vast_tag, click trackers, etc.). Whenreq.media_buy_id+req.package_idare also set, generate placement-specific tags with macro substitution baked in. - Inline build:
req.creative_manifestis 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.).
Sync at the wire level — the per-tool
build-creative-response.jsononeOfdoesn't include aSubmittedarm (spec inconsistency tracked asadcontextprotocol/adcp#3392). Until the spec rolls Submitted into theoneOf, slow tag-generation pipelines await in-request; status changes flow viactx.publish_status_change.Return shape: see :meth:
CreativeBuilderPlatform.build_creative()for the discriminated-arm rules — single :class:CreativeManifest,Sequence[CreativeManifest]for multi-format, or :class:BuildCreativeSuccessResponseenvelope when you needsandbox/expires_at/preview. - Library lookup:
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_reportstatus changes viactx.publish_status_changewhen 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. Whenreq.include_pricing, include vendor pricing options on each creative. def preview_creative(self, req: PreviewCreativeRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[PreviewCreativeResponse]-
Expand source code
def preview_creative( self, req: PreviewCreativeRequest, ctx: RequestContext[TMeta], ) -> MaybeAsync[PreviewCreativeResponse]: """Preview-only variant — sandbox URL or inline HTML, expires. Always sync. NOT optional for ad-server adopters (distinct from :class:`CreativeBuilderPlatform.preview_creative`, which is optional) — buyers expect preview surface from any stateful creative library. """ ...Preview-only variant — sandbox URL or inline HTML, expires.
Always sync. NOT optional for ad-server adopters (distinct from :class:
CreativeBuilderPlatform.preview_creative(), which is optional) — buyers expect preview surface from any stateful creative library. 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:
SyncCreativesSuccessResponsefor the sync fast path ORctx.handoff_to_task(fn)for HITL — brand-suitability, S&P review.action: 'created'for new entries,'updated'for replacements,'unchanged'when matching. Optionalstatus: '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( self, req: BuildCreativeRequest, ctx: RequestContext[TMeta], ) -> MaybeAsync[BuildCreativeSuccessResponse | 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 a :class:`BuildCreativeSuccessResponse` with ``sandbox`` / ``expires_at`` / ``preview`` populated. Framework passes through unchanged. 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( self, req: PreviewCreativeRequest, ctx: RequestContext[TMeta], ) -> MaybeAsync[PreviewCreativeResponse]: """Preview-only variant — sandbox URL or inline HTML, expires. Always sync. Optional — generative-only adopters that don't render preview ahead of generation can omit it; the framework returns ``UNSUPPORTED_FEATURE`` to buyers calling ``preview_creative`` against a platform that didn't wire this. """ ... 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
Tdirectly) or async (returnAwaitable[T]); the dispatch adapter detects via :func:asyncio.iscoroutinefunctionand runs sync methods on a thread pool.Throw :class:
AdcpErrorfor buyer-fixable rejection (UNSUPPORTED_FEATUREfor missing optionals,POLICY_VIOLATIONfor buyer rights issues, etc.); the framework projects to the wire structured-error envelope.Ancestors
- typing.Protocol
- typing.Generic
Methods
def build_creative(self, req: BuildCreativeRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[BuildCreativeSuccessResponse | Sequence[CreativeManifest] | CreativeManifest]-
Expand source code
def build_creative( self, req: BuildCreativeRequest, ctx: RequestContext[TMeta], ) -> MaybeAsync[BuildCreativeSuccessResponse | 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 a :class:`BuildCreativeSuccessResponse` with ``sandbox`` / ``expires_at`` / ``preview`` populated. Framework passes through unchanged. 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 onreqshape.Return shape is discriminated by the wire spec's Single vs Multi response arms:
- Single manifest, no metadata: return a :class:
CreativeManifestdirectly. Framework wraps as{creative_manifest: <manifest>}. Use this for single-format requests (target_format_id) when you don't need to setsandbox/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 a
:class:
BuildCreativeSuccessResponsewithsandbox/expires_at/previewpopulated. Framework passes through unchanged.
Adopters route on
req.target_format_ids(multi) vsreq.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). - Single manifest, no metadata: return a :class:
def preview_creative(self, req: PreviewCreativeRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[PreviewCreativeResponse]-
Expand source code
def preview_creative( self, req: PreviewCreativeRequest, ctx: RequestContext[TMeta], ) -> MaybeAsync[PreviewCreativeResponse]: """Preview-only variant — sandbox URL or inline HTML, expires. Always sync. Optional — generative-only adopters that don't render preview ahead of generation can omit it; the framework returns ``UNSUPPORTED_FEATURE`` to buyers calling ``preview_creative`` against a platform that didn't wire this. """ ...Preview-only variant — sandbox URL or inline HTML, expires.
Always sync. Optional — generative-only adopters that don't render preview ahead of generation can omit it; the framework returns
UNSUPPORTED_FEATUREto buyers callingpreview_creativeagainst a platform that didn't wire this. 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:SyncCreativesSuccessResponsefor the sync fast path ORctx.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_FEATUREwhen 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
Tdirectly) or async (returnAwaitable[T]); the dispatch adapter detects via :func:asyncio.iscoroutinefunctionand runs sync methods on a thread pool so a blocking sync handler doesn't serialize the event loop.Throw :class:
AdcpErrorfor 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:
GetSignalsResponsedirectly for the sync fast path. Brief-driven discovery MAY hand off viactx.handoff_to_task(fn)when provider discovery needs background work (cross-provider fan-out, identity-graph lookups); the framework projects the handoff to the wiresubmittedenvelope. The buyer reaches the terminal :class:GetSignalsResponseviatasks/getpolling, and — when the request carriedpush_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 withAdcpError(INVALID_REQUEST, field='discovery_mode').Note
get_signalsships ONLY thesubmittedandworkingasync arms — it has noinput_requiredarm (unlikeget_productsandcreate_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
Tdirectly) or async (returnAwaitable[T]); the dispatch adapter detects via :func:asyncio.iscoroutinefunctionand runs sync methods on a thread pool.Throw :class:
AdcpErrorfor 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_tokenthe buyer stores in their secret manager. Token is scoped to thislist_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_tokenimmediately and signal cache invalidation to sellers (reducedcache_valid_untilor 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_tokenfrom :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( self, req: ListCreativeFormatsRequest, ctx: RequestContext[TMeta], ) -> MaybeAsync[ListCreativeFormatsResponse]: """Catalog of accepted creative formats. Required when claiming any ``sales-*`` specialism in v6.0 rc.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 NotImplementedErrorUnified hybrid interface for every
sales-*specialism.Methods may be sync (return
Tdirectly) or async (returnAwaitable[T]); the dispatch adapter detects via :func:inspect.iscoroutinefunctionand runs sync methods on a thread pool via :func:asyncio.to_threadso 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:TaskHandoffvia type-identity and projects to the wireSubmittedenvelope.Throw :class:
AdcpErrorfor 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:
CreateMediaBuySuccessResponsedirectly for sync fast path; return :meth:RequestContext.handoff_to_taskfor 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_idfield present → sync successtask_id+status='submitted'→ polltasks_getor receive webhook
Framework injection: when a :class:
~adcp.decisioning.ProductConfigStoreis wired viaconfig_store=in :func:adcp.decisioning.serve.create_adcp_server_from_platform, the framework calls the store before invoking this method and injects the result asconfigs: dict[str, dict[str, Any]]. Declareconfigsin 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.packagesisNone(proposal-id flow),configsarrives 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_platformfails 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:
GetProductsResponsedirectly for the sync fast path. Brief / refine discovery MAY hand off viactx.handoff_to_task(fn)when composing the catalog needs background work (custom curation queue, slow proposal generation); the framework projects the handoff to the wiresubmittedenvelope. The buyer reaches the terminal :class:GetProductsResponseviatasks/getpolling, and — when the request carriedpush_notification_config— the framework also delivers the terminal completion / failure webhook to the buyer's URL from the background completion path (exactly once).get_productsexposes thesubmitted/working/input_requiredasync 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'stime_budgetdeclares the gap viaincomplete[]on a sync response rather than handing off; returning a handoff from a wholesale call is rejected at the framework layer withAdcpError(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 viactx.publish_status_change(resource_type='proposal', ...)rather than blockingget_productswaiting 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 implementrefine_get_productsrejectbuying_mode='refine'withAdcpError(INVALID_REQUEST, field='buying_mode')at the framework layer — the platform method is not called. def list_creative_formats(self, req: ListCreativeFormatsRequest, ctx: RequestContext[TMeta]) ‑> MaybeAsync[ListCreativeFormatsResponse]-
Expand source code
def list_creative_formats( self, req: ListCreativeFormatsRequest, ctx: RequestContext[TMeta], ) -> MaybeAsync[ListCreativeFormatsResponse]: """Catalog of accepted creative formats. Required when claiming any ``sales-*`` specialism in v6.0 rc.1+. """ ...Catalog of accepted creative formats.
Required when claiming any
sales-*specialism in v6.0 rc.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 NotImplementedErrorSync product catalogs with the platform.
Required when claiming
sales-catalog-driven.validate_platformhard-fails at server boot when this method is absent on asales-catalog-drivenplatform.Discovery mode: when
req.catalogs is None, return the account's existing synced catalogs without modification (read-only path per AdCP spec). Checkreq.catalogsbefore 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=Truewithreq.catalogs=Noneis spec-undefined — reject it withAdcpError("INVALID_REQUEST", field="catalogs")rather than silently deleting buyer-managed catalogs.Return a list of :class:
~adcp.types.SyncCatalogResultrows (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
Submittedarm yet (adcp#3392). Re-approval flows return the success with thestatusfield omitted (in-spec per the schema description) and drive lifecycle viactx.publish_status_change. v6.1 + adcp#3392 expand this signature to :data:SalesResultso 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:OwnedSignalsPlatformforsignal-ownedplatforms where returned signals are already usable in later media-buy targeting and there is no buyer-triggered destination provisioning step.Ancestors
- OwnedSignalsPlatform
- typing.Protocol
- typing.Generic
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
deploymentsrows in their current state —'pending'is a valid sync return for slow activation pipelines.Subsequent state changes (per-deployment
activating/deployed/failed) flow viactx.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'(unknownsignal_agent_segment_id),code='POLICY_VIOLATION'(buyer lacks rights to activate this data), orcode='INVALID_REQUEST'(missing or unrecognized destination).
Inherited members