> ## Documentation Index
> Fetch the complete documentation index at: https://docs.apostra.com/llms.txt
> Use this file to discover all available pages before exploring further.

# How Apostra uses AdCP (buy side)

> How Apostra, as a buyer platform, uses the Ad Context Protocol to discover and transact across many sellers — and where it conforms, extends, or doesn't implement a surface.

Apostra is built on the [Ad Context Protocol (AdCP)](https://docs.adcontextprotocol.org). This page covers the
**buy side** — how Apostra, acting as a buyer platform, *consumes* AdCP across many sellers.

<Info>
  This is the buy-side view. For the seller (supply) side — connecting your own AdCP sales agent as a Seller Account
  source — see [Connect your sales agent](/v2/storefront/inventory-sources/connect-your-agent). When this page and
  the AdCP spec disagree on what the *protocol* means, the AdCP spec wins.
</Info>

<Warning>
  The external-buyer campaign spine described below is alpha. It supports
  governed create, update, cancellation, and live delivery through
  platform-composed hosted Sellers and direct-adapter Sellers when the buyer
  passes the Seller's commercial checks. Listing + Distribution remains the
  Seller's public discovery product; it is not required for campaign writes.
  Adding packages during an endpoint update, change webhooks, and scheduled
  reporting exports are not part of this slice.
</Warning>

## Two AdCP directions, one direct campaign mode

AdCP addresses one Seller at a time. Whether the Seller is reached through
an external buyer endpoint or a connected account, the campaign carries
`mode: "directed"` — a compatibility wire value; read the campaign's
`management` state for what the platform does with it:

| Direction | Source of truth | Campaign meaning |
| - | - | - |
| An external AdCP buyer calls an Apostra-hosted Seller | The Seller Account manages execution; Apostra supplies the commercial and reporting envelope | **Managed campaign** (`management: "managed"`) — `mode: "directed"`, legacy wire provenance `directed.provenance: "storefront_endpoint"` |
| An Apostra buyer connects and mirrors an external seller/ad platform | The external seller owns the campaign; Apostra reconciles a mirror | **Tracked campaign** (`management: "tracked"`) — `mode: "directed"`, `directed.provenance: "connected_account"` |

A task addressed directly to one Seller carries `mode: "directed"` because
Apostra does not perform cross-Seller discovery or allocation. A
discovery campaign can still finish with one seller when Apostra performed
that selection; the result count does not change its mode. See
[Campaigns on connected provider accounts](/v2/buyer/campaigns/directed-campaigns)
for the shared model.

You may also hear this called a *mediated AdCP campaign*. Mediation is the platform
invariant, so these docs use *inbound* when distinguishing the protocol direction.
Neither “inbound” nor “mediated” is a campaign `mode`.

## The cross-seller pattern

The defining difference between AdCP and Apostra on the buy side is the same everywhere: **AdCP defines a task
against one agent; Apostra runs it across many.** That fan-out is our core scaling layer.

| AdCP task (single seller) | Apostra (many sellers) |
| - | - |
| `get_products` | **Get products across Sellers** — one canonical request fanned out to named or all connected Sellers, returned as a bounded canonical page with qualified IDs and optional proposal screening |
| `get_products` + session presentation | **Discover Products** — the same raw fan-out projected into grouped products, summary/budget context, browse, and SSE; no separate ranking brain |
| `get_signals` | cross-agent signal discovery |
| `create_media_buy` / `update_media_buy` | `create_media_buys` and a **Campaign** cart that coordinate one bilateral media buy per seller |
| `get_media_buy_delivery` | aggregated reporting rolled up across all sellers |

An external AdCP client can also call one Apostra-hosted Seller directly.
That is deliberately a single-seller protocol interaction, while Apostra still
provides the buyer identity, contract, governance, audit, campaign, and ledger around
the seller call.

### Compliance at the edges, scale above them

Apostra does not ask a seller to understand a multi-seller request. The
buyer-facing `ext.interchange` object selects Sellers, controls progressive
polling, and optionally requests managed screening. Apostra then projects
the canonical part of the request onto separate one-to-one `get_products`
calls. Each seller returns its normal AdCP products/proposals; Apostra
validates that response independently before aggregation.

The aggregate page stays bounded rather than becoming one giant catalog:

* `pagination.max_results` caps proposal and proposal-less-product candidates;
* products referenced by a paged proposal travel with that proposal so its
  allocation graph is not split;
* revisioned snapshots expose fast sellers while slower sellers remain pending;
* cursors become stable only after all reachable sellers settle; and
* storefront-qualified `sf1:` / `sfp1:` identities prevent collisions and are
  preserved until the bilateral purchase edge.

Optional screening is also above the protocol. It evaluates each valid proposal
against buyer instructions and returns `accept`, `reject`, or `refine`. A refine
decision is sent back to the originating seller through canonical AdCP; only the
seller can change pricing, allocations, availability, or terms.

## Conformance (buy side)

**Status:** **1:1** conforms as written · **+ext** with an Apostra scaling layer · **none** not implemented yet.

| AdCP surface | Status | What Apostra does |
| - | - | - |
| [`get_products`](https://docs.adcontextprotocol.org/docs/media-buy/task-reference/get_products) | 1:1 edges / +ext buyer | Sends the canonical request separately to each selected Seller, then adds Seller selection, progressive aggregation, qualified identity, and optional proposal screening above those bilateral calls. See [Get products across Sellers](/v2/buyer/discovery/multi-storefront-get-products). |
| [`sync_catalogs`](https://docs.adcontextprotocol.org/docs/media-buy/task-reference/sync_catalogs) | 1:1 | Matches the spec; the catalog is also injected into the discovery fan-out. |
| [`create_media_buy`](https://docs.adcontextprotocol.org/docs/media-buy/task-reference/create_media_buy) | +ext | Through the buyer API, a platform-managed **Campaign** can spawn one media buy per Seller. On an enrolled hosted-Seller endpoint, the strict AdCP request creates one governed campaign anchor—projected as `mode: directed` for compatibility—before Seller dispatch. See [Create a campaign](/v2/buyer/campaigns/tasks/create-campaign). |
| [`update_media_buy`](https://docs.adcontextprotocol.org/docs/media-buy/task-reference/update_media_buy) | +ext | Campaign-level update cascade; a capability guard blocks updates an upstream agent can't support. |
| [`get_media_buy_delivery`](https://docs.adcontextprotocol.org/docs/media-buy/task-reference/get_media_buy_delivery) | +ext | Aggregated reporting across all sellers. |
| [`get_media_buys`](https://docs.adcontextprotocol.org/docs/media-buy/task-reference/get_media_buys) | +ext | Campaign-granularity list; most-restrictive status rollup. |
| `list_creatives` / [`build_creative`](https://docs.adcontextprotocol.org/docs/creative/task-reference/build_creative) | +ext | Campaign-scoped manifests; conversational creative sessions. |
| `get_signals` | +ext | Cross-agent signal discovery. |
| [`sync_audiences`](https://docs.adcontextprotocol.org/docs/media-buy/task-reference/sync_audiences) / [`sync_event_sources`](https://docs.adcontextprotocol.org/docs/media-buy/task-reference/sync_event_sources) / [`log_event`](https://docs.adcontextprotocol.org/docs/media-buy/task-reference/log_event) | +ext | First-party audience + event stores feeding the Measurement Engine. |
| Measurement taxonomy | +ext | A Bayesian incrementality engine. See [Measurement & incrementality](/v2/guides/measurement-incrementality). |
| `list_accounts` / `sync_accounts` | +ext / 1:1 | A three-stage buyer model (credentials → available → linked). |
| [`provide_performance_feedback`](https://docs.adcontextprotocol.org/docs/media-buy/task-reference/provide_performance_feedback) | none | Not implemented; an internal optimization loop substitutes. |
| [`get_creative_delivery`](https://docs.adcontextprotocol.org/docs/creative/task-reference/get_creative_delivery) | none | Not implemented; reporting currently stops at the package level. |

## External buyer to Apostra to seller (alpha)

An external AdCP client follows the Seller's Discovery Card action. On a
verified Seller CNAME the endpoint is:

```text theme={null}
https://ads.seller.example/adcp/mcp
```

The platform-managed fallback is:

```text theme={null}
https://api.apostra.com/seller/{platformId}/mcp
```

`/adcp/mcp` is deliberately stable and unversioned. MCP/AdCP negotiates a
compatible protocol version directly, avoiding redirects for authenticated MCP
POST requests. Every hosted Seller has one deterministic endpoint. The endpoint
is the Seller's external demand surface for AdCP buyers: it exposes that
Seller's products and directed media-buy lifecycle, rather
than performing Apostra's cross-seller discovery or allocation. The
endpoint can exist before the Seller has Listing + Distribution. Campaign
write tasks remain fail-closed until terms, payment authority, rate-card, and
Seller approval rules admit the request.

Use the normal authenticated AdCP 3.0/3.1 task lifecycle:

1. Call `get_adcp_capabilities` and `get_products` on that Seller.
2. Send `create_media_buy` with an `idempotency_key`, brand identity, flight,
   currency, and one or more explicitly budgeted packages.
3. Use the returned `media_buy_id` with `get_media_buys` and
   `get_media_buy_delivery`.

### Country and channel routing

For an external sales agent, Apostra reads portfolio coverage from
`get_adcp_capabilities.media_buy.portfolio`:

* `primary_channels` and `primary_countries` are the declared coverage for that
  source. Apostra keeps the two arrays correlated, and accepted countries
  equal primary countries.
* Coverage from separate sources is never combined to manufacture a match.

There is no separate local accepted-country allowlist for a pure pass-through storefront.
After the staged buyer-routing flag is enabled, Apostra includes a source
only when both primary arrays are complete and match the requested coverage. A
missing or malformed primary array is unknown and is a go-live readiness gap.
Before that flag is enabled, legacy product fan-out remains unchanged.

Missing primary coverage means **unknown**, not global coverage. Apostra
does not infer an upstream agent's countries from the storefront listing or
business profile.

### Linked account scope

When a request carries a linked storefront `account_id`, Apostra resolves
the authenticated buyer's grant before contacting any inventory source. Each
source receives only the account identifier the seller assigned to that source;
the linked Apostra account ID is never forwarded as if it were a source
account.

`get_products` returns structured AdCP errors when the account cannot be used:

| Code | `details.reason` | What to do |
| - | - | - |
| `AUTH_REQUIRED` | `account_requires_principal` | Authenticate as the buyer, or omit the account for public discovery. |
| `AUTH_REQUIRED` | `account_grant_inactive` | Complete or restore account approval, then retry. |
| `NOT_FOUND` | `account_context_not_found` | Verify the account ID or request a new linked account. The response does not reveal whether an account belongs to another buyer or storefront. |
| `SERVICE_UNAVAILABLE` | `account_context_unavailable` | Retry the same request after a short delay. |

Missing, ambiguous, or stale source mappings are excluded from discovery
without calling those sources. If every source required by `create_media_buy`
lacks a healthy mapping, the tool returns `ACCESS_DENIED` with
`details.reason: "source_account_not_bound"` and the coverage state in
`details.per_source`. The seller must repair the mapping; the buyer then runs
`get_products` again and submits a new media buy. Calls without a linked account
and legacy natural-key accounts are unchanged. A transient account-resolution
failure instead returns `SERVICE_UNAVAILABLE` with
`details.reason: "account_context_unavailable"`; retry the same request after a
short delay rather than changing the mapping.

The same linked account now scopes media-buy reads. Call `get_media_buys` with
the opaque `account_id` and no `media_buy_ids` to enumerate the mapped account
across every inventory source that has a healthy native-account mapping.
Apostra drains each source's pages within a bounded discovery window,
qualifies colliding native IDs into stable storefront `media_buy_id` values,
and persists the route so
`get_media_buy_delivery` can reach the same source later. Pass the same account
and buyer credential on the delivery call; Apostra substitutes the
source-native account again. To include terminal history, supply the desired
`status_filter` values—an omitted filter keeps each source's normal AdCP
default. A targeted `get_media_buys` call with `media_buy_ids` reads already
discovered storefront IDs and does not re-enumerate sources. Follow
`pagination.cursor` to read the complete combined result.

If one mapped source is unavailable, the completed response contains the buys
from healthy sources plus a generic `SOURCE_UNAVAILABLE` entry; it never exposes
the source name, native account ID, or provider error. If every mapped source is
unavailable and there is no persisted result to return, the task fails with
`SERVICE_UNAVAILABLE` and
`details.reason: "account_media_buy_sources_unavailable"`. Sources marked
`not_required` are not enumerated for account history: an unscoped campaign list
could expose other buyers. Account-enumerated buys are tracked, read-only
records; create a managed buy through the normal product and media-buy flow
instead of updating a discovered buy. Webhook activity is not included in this
surface because the upstream call uses the storefront's source credential, not
the linked buyer principal.

On a platform-composed storefront endpoint, the same linked account can list
audience, signal, and event-source references with Apostra MCP extension
`get_account_resources`. Direct adapter storefronts do not expose this tool.
This is a live, read-only call: Apostra verifies the active grant for the
authenticated buyer principal before contacting a source, forwards each
eligible source only its own native account reference, and returns opaque
storefront-qualified resource IDs. It never returns private source IDs,
source-native account IDs, audience membership, setup secrets, or event
payloads.

| Resource type | Provider read | Authority and coverage |
| - | - | - |
| `signal` | Account-scoped `get_signals` wholesale discovery | Authoritative only when the provider explicitly reports an account cache scope. Missing or public cache scope is labeled non-authoritative and partial. |
| `audience` | Discovery-only `sync_audiences` (no audience payload) | Returned references are authoritative, but coverage is labeled `partial` because provider enumeration differs. TikTok exhausts the selected advertiser's Customer File inventory; tenant-owned governed rows use canonical buyer IDs, while unmanaged rows remain provider inventory references. |
| `event_source` | Discovery-only `sync_event_sources` (no event-source payload) | Buyer-managed sources are returned as references. Seller-managed sources are excluded and counted in coverage. No event data or setup secret is returned. |

Every source/resource-kind pair reports `capability`, `coverage`,
`authoritative`, `observed_at`, item/exclusion counts, truncation, and a generic
error code when a provider read fails. Missing, ambiguous, stale, and
`not_required` source mappings are never called. Demo or placeholder resources
are explicitly labeled non-authoritative. Results default to 50 resources and
accept `pagination.max_results` up to 100; follow the opaque cursor while
`pagination.has_more` is true. A request is also bounded by a 30-second live-read
window and a 1,000-resource safety ceiling, so `partial` or `truncated` must be
checked before treating the result as a complete account inventory.

This is different from the tracked-campaign `.../presences` endpoint. That
endpoint reads a background mirror for subscribed connection accounts;
`get_account_resources` uses the linked account's principal grant directly.

The same buyer credential must resolve to an Apostra buyer account. A
transacting credential should be scoped to an advertiser. During the alpha, an
unscoped credential may resolve by `brand.domain` only when that domain identifies
exactly one active advertiser in the buyer account; missing or ambiguous identity
fails closed.

### What Apostra adds before seller dispatch

For an enrolled relationship, media-buy writes do more than relay the protocol
request:

* applies any spend cap Scope3 has set for the buyer on that storefront: a
  currency plus per-buy and aggregate exposure caps. Without one, Apostra adds no
  cap of its own, and the seller's terms, payment authority, rate card and approval
  policy decide whether the buy goes ahead;
* journals create, update, and cancellation idempotently so an uncertain seller
  outcome is not silently dispatched twice;
* creates the compatibility `mode: directed` campaign projection, media buy, and package records in the
  buyer account;
* pins the buyer's zero-fee AdCP entitlement terms for the life of the buy;
* commits the authorized amount to the campaign ledger after the seller accepts or
  begins processing the buy; and
* records dark alpha usage at zero billable amount.

Zero-billable alpha usage applies to the feature's usage meter, not to media. The
authorized media budget and zero-fee AdCP entitlement still govern the buy.

Every caller budget is **gross**: the all-in amount authorized by the buyer. Pure
AdCP access pins a zero fee, so gross and net are equal and the full media budget
reaches the seller. Ordinary buyer clearing fees are not inherited on this path;
advanced value is governed by entitlements/IUs. The seller never receives buyer
terms. See
[Budgets and fees](/v2/concepts/budgets-and-fees).

The route is dual-keyed. Apostra campaign/media-buy identity drives contract,
governance, and reporting records; the AdCP media-buy identity correlates the public
task response and downstream seller object. `get_media_buys` and
`get_media_buy_delivery` therefore remain addressable with the external buyer's
AdCP ID without making the seller's ID the buyer-account primary key.

### Current reporting behavior

`get_media_buy_delivery` is live read-through: Apostra calls the seller, returns
the current delivery response, and opportunistically records realized spend against
the pinned zero-fee entitlement ledger. The alpha does not promise a cached time series, change
webhooks, scheduled delivery, or billing UI from that read.

<Warning>
  Any future non-zero buyer-edge fee requires separate fee-inclusive delivery
  verification before rollout. The current alpha remains zero-fee and is not an invoice
  statement.
</Warning>

### Current eligibility and limits

* The buyer campaign spine is fail-closed on commercial checks: terms, payment
  authority, the active rate card, and Seller approval rules. It is not enabled
  buyer by buyer, and there is no environment-variable bypass. Listing +
  Distribution continues to control public discovery only.
* Every admitted storefront route creates the governed campaign, contract,
  operation, and ledger anchor before dispatch. This includes platform-composed
  storefronts and direct social/ad-platform `ADAPTER` storefronts. Wonderstruck ESA
  is the first live managed-agent proof; adapter routes also require a delegated,
  advertiser-mapped provider account.
* Seller rejection and non-retryable dispatch failures mark the campaign as failed.
  A temporary source or adapter outage remains pending while Apostra retries for
  up to six hours, then becomes failed if the retry window ends without a successful
  dispatch.
* The governed endpoint spine covers `create_media_buy`, supported
  `update_media_buy` fields, and cancellation. Update/cancel share the pinned policy,
  durable mutation journal, budget-delta caps, SCD2 activation, dark metering,
  ledger commitment sync, and ambiguous-outcome fence. Adding packages still returns a
  capability error.
* Strict SDK lifecycle testing and source-specific end-to-end verification are
  both required before admitting another Seller's storefront to campaign writes.

## Where the truth lives

<CardGroup cols={2}>
  <Card title="AdCP specification" href="https://docs.adcontextprotocol.org" icon="book">
    The protocol's source of truth.
  </Card>

  <Card title="Connect your sales agent" href="/v2/storefront/inventory-sources/connect-your-agent" icon="plug">
    The seller (supply) side — bring your AdCP agent in as a storefront source.
  </Card>

  <Card title="Get products across storefronts" href="/v2/buyer/discovery/multi-storefront-get-products" icon="network-wired">
    Canonical AdCP-shaped retrieval with Apostra fan-out, screening, refinement, and purchase continuation.
  </Card>

  <Card title="Discover Products" href="/v2/buyer/discovery/tasks/discover-products" icon="magnifying-glass">
    The legacy-compatible grouped, browsable discovery-session projection.
  </Card>

  <Card title="Create a campaign" href="/v2/buyer/campaigns/tasks/create-campaign" icon="bullhorn">
    How a campaign spawns media buys across sellers.
  </Card>

  <Card title="Budgets and fees" href="/v2/concepts/budgets-and-fees" icon="sack-dollar">
    Why the buyer budget is gross while the seller receives net media spend.
  </Card>

  <Card title="Campaigns on connected provider accounts" href="/v2/buyer/campaigns/directed-campaigns" icon="arrows-rotate">
    Tracked campaign mirrors — the opposite ownership direction.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.