> ## 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.

# v3 Tool Catalog

> The stable tool families exposed by the account-resolved v3 MCP endpoint.

<Note>
  `tools/list` is authoritative for the active account. A principal with both
  Buyer and Seller access receives their deduplicated union at connection time,
  while calls remain authorized against the selected account. Enrollment,
  permissions, feature rollout, and host support can narrow the catalog. This
  page explains the complete registered families and intended use.
</Note>

## Navigation and shared tools

The [Identity and brands guide](/v2/setup/v3/identity-setup) covers current
identity and provenance, domain correction, public brand lookup, confirmation
and human proof follow-up in text-only clients. These operations use
`get_status`, `get`, `save_buyer_operator`, `save_advertiser` and `save_seller`.
Select `identityContract: "confirmed-v1"` for the preview and confirmation
workflow; existing save payloads keep their earlier contract.

`switch_account` works for a signed-in user on both session-based and
reconnecting MCP clients. After a successful switch, call `get_status` to read
the active account and its available tools; the selected account remains active
when the client reconnects.

| Tool | Accounts | Purpose |
| - | - | - |
| `get_status` | Every account | Read active account, readiness, blockers, next actions, and reachable accounts |
| `switch_account` | Every account | Move to a reachable account or return home |
| `save_ask` | Every account | File or update support, product, supply, integration, or commercial asks |
| `save_notification_config` | Shared, when enabled | Save typed notification thresholds and preferences |
| `search` | Buyer, Seller, and Organisation | Search supported kinds, documentation, or the AdCP specification; Organisation accounts receive documentation search only |
| `get` | Every account | Read an object or singleton; Organization accounts expose billing only |
| `open_page` | Buyer and Seller | Open an account-appropriate MCP App Page |
| `test_creative_macros` | Buyer and Seller | Dry-run one tracker URL from raw vendor syntax through AdCP, recipient syntax, and synthetic values |
| `open_connections_page` | Buyer | Open Media Partners, optionally seeded by a known positive integer-string `advertiserId` or `sellerId`; `connectionAction: "connect"` opens setup and requires `sellerId` |
| `open_creative_engines_page` | Buyer | Open Creative Engines, optionally focused on a registered positive integer-string `engineId` or an existing `connectionId`; `connectionAction: "connect"` requires `engineId` and focuses secure setup, which the buyer starts in the Page |
| `open_advertisers_page` | Buyer | Open the Advertisers Page — every advertiser with campaign and draft counts and one next action each; see [Open Advertisers](/v2/buyer/advertisers/tasks/open-advertisers-page) |
| `open_campaigns_page` | Buyer | Open the Campaigns Page, optionally scoped by a positive integer-string `advertiserId` or focused by `campaignId`; see [Open Campaigns](/v2/buyer/campaigns/tasks/open-campaigns-page) |
| `open_campaign_receipt` | Buyer | Open Review & go live for one draft `campaignId` — plan, staged media buys, and readiness blockers; refuses non-draft campaigns; see [Open Review & go live](/v2/buyer/campaigns/tasks/open-campaign-receipt) |
| `open_approvals` | Seller | Open the seller's governed approvals Page |
| `open_media_buys_page` | Seller | Open Media Buys, optionally scoped to one seller-owned account relationship |
| `open_seller_dashboard` | Seller | Open Seller Dashboard: seller analytics, or the Demo Storefront evaluation view while the account holds an active Demo Storefront lease; takes no arguments |
| `open_proposal_pass` | Seller | Open the portable Proposal Pass Page for one exact immutable `rfpId`/`turnId` pair; see [RFP lifecycle](/v2/setup/v3/rfp-lifecycle#proposal-pass-the-portable-turn-workbench) |
| `save_session` | Platform operator in a seller account | Persist an idempotent private coaching note on one retained native Murph Session; it does not send a customer message |

### Catalogs

Buyer accounts can search and read their advertiser-scoped catalogs with
`search(kind: "catalog")` and
`get(kind: "catalog", id, advertiserId: "12345")`. Use
`save_catalog` to declare one AdCP Catalog or archive it. Item reads are a
bounded `get` include and never expose feed credentials.

### Open Media Buys

`open_media_buys_page` opens the Seller Media Buys Page. It accepts an optional
positive-integer-string `accountRelationshipId` and optional `view` of
`media_buys`, `creatives`, or `delivery`. With no relationship ID, it opens the Seller's
unfiltered Media Buys Page.

When a relationship ID is supplied, the tool first proves that the relationship
belongs to the authenticated Seller Account. An unknown or another seller's
relationship fails closed: it returns no Page directive and opens nothing. The
Page then shows only that buyer relationship's activity with the Seller — its
media buys and synced creatives — never the buyer's own campaigns.

### Open Seller Dashboard

`open_seller_dashboard` opens Seller Dashboard. It takes no arguments and is
listed only for Seller Accounts; a call from another account returns the
`switch_account` call that reaches a Seller Account and opens nothing.

The Page chooses its own view. While the Seller Account holds an active, unexpired
[Demo Storefront](/v2/storefront/inventory-sources/demo-storefront) lease, it
opens the **Demo Storefront evaluation** view of practice runs, labelled as
synthetic evaluation data. Opening it does not make the Demo Storefront live.
Every other Seller Account sees its seller analytics. The Page loads its data
through its own app-only reads.

Every `search` call must provide `query`, `kind`, or both. An empty call is
invalid. Use `query` for a match term or documentation question and `kind` to
list account objects.

### Campaign filters

`search(kind: "campaign")` pages the complete filtered campaign collection.
Pass the returned `nextCursor` with the same filters to continue; `total` is
the filtered total and `truncated` is true only when another page remains.

| Field | Type | Notes |
| - | - | - |
| `filter.campaignStatus` | array | Any of `needs_attention`, `active`, `draft`, `completed`, `paused`, `canceled`, or `archived`. Statuses are ORed; `archived` selects archived visibility rather than an ordinary lifecycle status. |
| `filter.campaignMode` | string | `managed` for platform-authored campaigns or `tracked` for provider-account mirrors. |
| `filter.campaignName` | string | Case-insensitive partial campaign-name match, applied before totals and cursor paging. |

A Buyer can browse the selected advertiser's visible products across eligible
Sellers before creating a campaign:

```json theme={null}
{
  "kind": "wholesale_product",
  "sources": ["objects"],
  "filter": { "advertiserId": "ADVERTISER_ID" }
}
```

Each result includes the exact opaque `productId`, `sellerId`, and Seller name
needed for a subsequent `creative_format` search. Pagination traverses each
qualified product once, even when multiple seller proposals reference it. Pass
`nextCursor` back with the same advertiser and `limit`. When `resultsComplete`
is false, replace the provisional page after polling; when `partial` is true,
do not treat an absent product as unavailable. `persistenceIncomplete`
identifies a terminal partial result where otherwise visible products were
withheld because a complete, buyer-safe identity could not be persisted;
restart without a cursor to retry them. This read may use live or cached
catalogue results and does not create a proposal, campaign, or media buy.

To see whether each product can be bought on the advertiser's connected ad
account, add `include: ["executionReadiness"]`:

```json theme={null}
{
  "kind": "wholesale_product",
  "sources": ["objects"],
  "filter": { "advertiserId": "ADVERTISER_ID" },
  "include": ["executionReadiness"]
}
```

Each row then carries `executionReadiness` when its seller reports readiness
(currently Meta, TikTok, Snap and LinkedIn): a summary of whether the product
can be bought on that account and what's missing.

```json theme={null}
{
  "status": "selection_required",
  "slots": [
    {
      "kind": "platform_identity",
      "decision": "explicit_selection_required",
      "total": 3
    },
    {
      "kind": "event_source",
      "decision": "bound_default",
      "total": 1,
      "selected": {
        "providerId": "123456789012345",
        "provenance": "deterministic_default"
      }
    }
  ]
}
```

`status` is `ready_for_execution`, `setup_required`, `input_required`,
`selection_required` or `inventory_unavailable`. Each slot names a resource
the product needs (such as a Facebook Page or a pixel), the `decision` for it,
an optional `reason`, the option already chosen in `selected` (when the seller
picked one), and `total`, the number of options available. The options
themselves aren't listed in this search. Parts of a seller's readiness that
don't match this shape are left out individually, so a row keeps the rest of
its summary. Rows from sellers that don't report readiness have no
`executionReadiness` field. Keep the same `include` when you pass `nextCursor`
back. The include works only for `wholesale_product` on a buyer account.

A Creative search must name exactly one owner. Use `filter.advertiserId` to
browse the advertiser's visible Creative summaries, or `filter.campaignId` to
list summaries attached to one campaign:

```json theme={null}
{
  "kind": "creative",
  "filter": {
    "advertiserId": "ADVERTISER_ID",
    "promoted": true
  }
}
```

Advertiser scope supports `formatKind`, `assetType`, `role`, `source`, and
`promoted`. Omitting `promoted` returns all visible Creatives; `true` selects
the promoted reusable shelf, and `false` selects non-promoted Creatives.
Campaign scope supports the top-level `query` only. Each successful response
repeats the effective owner scope and filters in both text and structured
output. Results are plain Creative summary rows; this search does not open or
return a Creative Library Page. Pass `nextCursor` back only with the same
selected account, owner scope, effective filters, and limit.

A Seller asking what it currently offers uses:

```json theme={null}
{ "kind": "wholesale_product" }
```

### Conversation history

Buyer and Seller members can explicitly search their prior Murph rooms with
`kind: "conversation"`:

```json theme={null}
{
  "kind": "conversation",
  "query": "campaign pacing",
  "filter": {
    "conversationStartedAfter": "2026-09-07T00:00:00Z",
    "conversationStartedBefore": "2026-09-08T00:00:00Z"
  }
}
```

The result includes only the member's own rooms and explicitly shared rooms in
the active account. Matches include bounded, display-safe `matchedText`
evidence from the matching title, participant, or message, even when a matching
message is older than the recent transcript tail. Follow a returned id with
`get({"kind":"conversation","id":"..."})` to read up to 20 recent,
display-safe user/assistant messages. That bounded detail read does not limit
the history searched. Tool output and system context are never returned.
Service credentials without an end-user identity fail closed, and conversation
search runs only when explicitly requested; it is not included in an
unqualified cross-kind search.

### Retained Sessions

Retained Session reads are available to signed-in, non-impersonated platform
operators in seller accounts. An ordinary seller can directly search or get
only an explicitly source-qualified `storefront_rfp_v1` Session. Buyer Session
access is not yet available because no source currently opts in to buyer
relationship checks. Catalogue visibility is unchanged from the current
release; buyer Session search requires a seller account and direct Session
reads are denied. When a source does opt in, buyers will see no result for
revoked relationships, other buyers' exchanges, or unrelated sellers' exchanges,
and results will exclude private events, seller private notes, and coaching.
`save_session` remains seller-only and can add an idempotent private coaching
note. See the
[Retained Session timeline](/v2/setup/v3/session-timeline) for access,
cursor, import-limit, and unsupported-operation boundaries.

### `save_session`: record a private note

`save_session` accepts only `action: "record_note"`. Use the exact
`sessionId` returned by Session search or read, a `clientRequestId`, and the
private `note`. `throughEventId` is optional; when supplied, it anchors the
note to one accessible retained event in that Session.

`sessionId`, `clientRequestId`, and `throughEventId` are opaque strings. They
are trimmed, must not be empty, and can contain at most 512 characters. The
private `note` is also trimmed and required, with a maximum of 8,000
characters.

```json theme={null}
{
  "action": "record_note",
  "sessionId": "SESSION_ID",
  "clientRequestId": "note-001",
  "throughEventId": "EVENT_ID",
  "note": "Explain the price before asking for a commitment."
}
```

The MCP `structuredContent` response is limited to the write result:

```json theme={null}
{
  "action": "record_note",
  "sessionId": "SESSION_ID",
  "receipt": null
}
```

It does not return the full Session or event, and it does not say whether a
note was created or replayed. Re-read the Session with
`get({"kind":"session","id":"SESSION_ID"})` to inspect the retained
note.

The same scoped request key with the same content replays the saved note.
Reusing that key with changed content returns a conflict. The tool never sends
an outbound message. A retry is denied when current access no longer permits
the note, and every action other than `record_note` is unsupported.

This tool is listed only for a signed-in platform operator in an active seller
account. It requires no user impersonation and the current native-room
permission check. The seller catalog entry does not expose it to customers;
the tool also rechecks the operator, end-user, and seller boundaries at call
time. See the [Retained Session timeline](/v2/setup/v3/session-timeline) for
the complete access and retry rules.

### Seller and product filters

For `search({"kind":"wholesale_product"})`, add `sourceId` to narrow
the list to one ad-server or modular source, or
`filter.productStatus` to select `draft`, `active`, or `archived` products.
Each product carries `origin`: `ad_server` for a product you author on an
ad-server source, and `modular_source` for one a modular source offers from its
avails. A modular source's products are the ones its storefront catalogue
serves to buyers and proposals. You change them at the source, not with
`save_wholesale_product`. Each product also carries `price`, the price a
proposal quotes for it, and a Demo Storefront product carries `synthetic`.
For Buyer seller reads, `filter.ids` accepts up to 50 exact Storefront ids in
one call, preserves requested order, and reports unavailable ids as
`objects.missingIds`.

As an alternative Buyer read scoped to one Seller, call `get` with
`kind: "seller"`, that exact Seller `id`, the current account's
`advertiserId`, and `include: ["products"]`. Preserve the returned
`productQueryId` and catalog revision when polling the same request to a
terminal result; do not reuse a Product from an older catalog.

Buyer `search(kind:"seller")` also accepts these filter fields to narrow results:

| Filter field | Type | Values / notes |
| - | - | - |
| `filter.classification` | string | `global_market_maker`, `regional_market_maker`, or `marketplace_seller` |
| `filter.marketplaceReady` | boolean | `true` returns only marketplace-listed, transaction-ready sellers |
| `filter.channel` | string\[] | Any channel the seller carries (e.g. `display`, `ctv`). Results include sellers that carry at least one of the listed channels. |
| `filter.region` | string | Region code the seller covers (e.g. `EMEA`, `NORAM`, `APAC`) |

`seller` and `connection` are intentionally separate buyer nouns. A seller is
one exact Storefront commercial counterparty and can exist before authorization.
A connection is one authorization grant, identified by its typed `target`.
Seller grants retain `sellerId`. Enrolled buyers can also connect a
[Creative Engine](/v2/setup/v3/creative-engines) using
`target: {kind: "creative_engine", id: "..."}` and search its grants with
`filter.targetKind: "creative_engine"`. Omit that filter to list seller grants.
One seller can have zero, one, or multiple connections, and each connection can
contain multiple discovered provider accounts. Use `search(kind:"connection")`
for questions such as “who am I connected to?” and join the returned `sellerId`
to the seller catalog when names or marketplace policy are needed.

Each connection reports how many advertisers are mapped to its active provider
accounts, and which ones, so a single `search(kind:"connection")` call answers
“is this advertiser mapped here?” without reading each connection.

| Field | Type | Notes |
| - | - | - |
| `mappedAdvertiserCount` | integer | Distinct advertisers mapped to one of the connection's active accounts |
| `mappedAdvertiserIds` | string\[] | Those advertisers' ids, ascending, at most 100 |
| `mappedAdvertiserIdsTruncated` | boolean | `true` when `mappedAdvertiserCount` exceeds the ids returned; `get(kind:"connection")` returns the full mapping rows, paged |

A `get(kind:"seller")` call for a single seller also returns
`connectionState.directedCampaignSubscription` when an active directed campaign
subscription exists between the buyer and that seller. The field is absent on
search results and null when no subscription exists.

| Field | Type | Notes |
| - | - | - |
| `connectionState.directedCampaignSubscription.status` | string | `ACTIVE`, `PAUSED`, or `ERROR` |
| `connectionState.directedCampaignSubscription.lastSyncedAt` | string \| null | ISO timestamp of the most recent sync |
| `connectionState.directedCampaignSubscription.lastSyncStatus` | string \| null | `SUCCESS` or `ERROR`; null before the first sync |

## Internal Marketplace administration

Directly authenticated Apostra SuperAdmins may also receive two internal tools:

| Tool | Purpose |
| - | - |
| `preview_marketplace_supply_batch` | Read up to 25 exact Storefronts in one batch and return current revisions, blockers, and proposed Market Maker scope |
| `apply_marketplace_supply_batch` | Apply the confirmed, revision-guarded governed reviews and return one outcome per Storefront |

These tools are omitted from every non-SuperAdmin catalog and re-check staff
authorization at execution. Apply performs a whole-batch stale-state preflight,
then commits each Storefront independently. Its response labels this behavior
`BEST_EFFORT_WITH_BATCH_PREFLIGHT`; callers must inspect every item before
retrying failures.

## Plan and billing

| Tool | Accounts | Purpose |
| - | - | - |
| `get` with `kind: "billing"` | Every account | Read billing readiness, current Terms and payment status |
| `save_billing` | Buyer and Seller | Accept Terms, choose Buyer payment terms when available, or use the existing request/confirm/status card-capture contract |
| `get_plan_billing_account` | Buyer, Seller and Organization | Read plan, agreement, and billing readiness |
| `accept_plan_billing_tos` | Buyer, Seller and Organization | Record authorized Terms acceptance |
| `set_plan_billing_payout_payee` | Buyer, Seller and Organization | Launch the governed payout-payee flow |

### Check and complete Terms setup in text-only chat

Call `get` with `{ "kind": "billing" }`. This read is available before Terms
acceptance, takes no ID or account override, and uses the selected account's
organization. Check `object.organization.customerId` before proposing a write.
The read uses the same billing facts and next-action order as Plan & Billing.

Both response channels contain the organization, readiness, Terms, payment
status and next action, so this flow works in hosts that only expose text.
The response omits card details, billing contact details, processor identifiers
and browser credentials.

| Field | Meaning |
| - | - |
| `terms.requiredVersion`, `terms.url` | Current Terms version and the public document to review |
| `terms.acceptanceRequired`, `terms.canAccept` | Whether acceptance is the current next action and this authenticated user has direct authority on this organization |
| `terms.currentVersionAccepted` | Whether this organization has an acceptance record for the current version. An existing contract or an honoured earlier version can mean acceptance is not required. |
| `nextAction` | The canonical action, explanation, documentation link and whether it can currently be completed in chat. `NONE` means no action to take now; read the explanation, which can describe a pending review. |
| `terms.acceptedVersion`, `terms.acceptedAt` | The Terms version this organization last accepted, and when. `save_billing` takes and returns the version under the same name. |
| `paymentAuthority` | Current verification, the latest capture link's `status` and `expiresAt`, submitted setup state and secure handoff availability |

When `nextAction.canCompleteInChat` is `true` and the person has reviewed the
Terms and explicitly authorizes acceptance, call `save_billing` with the exact
`terms.requiredVersion` from that read.
This JavaScript example uses the structured response; a text-only host gets
the same version in the `required version` line:

```javascript theme={null}
const read = await callTool("get", { kind: "billing" });
if (read.structuredContent.object.nextAction.canCompleteInChat) {
  await callTool("save_billing", {
    terms: {
      accepted: true,
      acceptedVersion: read.structuredContent.object.terms.requiredVersion,
    },
  });
}
const after = await callTool("get", { kind: "billing" });
```

`save_billing` accepts exactly one intent per call: `terms`, `paymentTerms`, or
`paymentAuthority`. Acceptance requires an authenticated user who is a direct
organization admin, within the credential's organization boundary. A child
account admin is insufficient without direct authority on the parent. A stale
version is rejected; read again and obtain authorization for the new Terms.
Use the subsequent read to verify the same `organization.customerId`, the
accepted version and any remaining action. A successful acceptance returns
`action: "terms_accepted"` with `terms.acceptedVersion`, the field the read
reports.

### Choose payment terms for consolidated billing

When `get_status` reports a required `payment_terms` next action, an
organization administrator must choose the terms that sellers are asked for
before the buyer can use consolidated billing. The next action names
`save_billing` with `paymentTerms: "net_60"`, which is the default choice; use
that value when the buyer wants the default. Otherwise, confirm the buyer's
choice and send one of `net_15`, `net_30`, `net_45`, `net_60`, or `net_90`:

```json theme={null}
{
  "paymentTerms": "net_45"
}
```

This capability is available only while the `buyer-payment-terms` rollout is
enabled for the account. Before then, `save_billing` refuses `paymentTerms`
with a validation error. Do not send `null` to restore a default; `null` is
refused. Send `net_60` instead.

The successful response has `action: "payment_terms_saved"` and echoes both
`paymentTerms` and `paymentTermsChosen: true`. The recorded choice belongs to
the billing organization, including when its administrator works in a child
Buyer Account context.

Organization (parent-container) sessions can read billing readiness, but their
`save_billing` calls remain blocked by the existing tool policy. For these
sessions, `terms.canAccept` describes the user's authority and
`nextAction.canCompleteInChat` is `false`. An organization admin completes
Terms and payout setup in the Plan & Billing Page instead: its
`accept_plan_billing_tos` and `set_plan_billing_payout_payee` actions run in
the Organization session. A payout payee set there is the organization's
default for every Seller Account without its own payee. The organization
must have at least one Seller Account.

If the account is already governed by a custom contract, no standard Terms
acceptance is recorded. The tool returns `action: "terms_not_required"` with
`terms.accepted: false` and `terms.governedByExistingContract: true` so the
agent does not report a legal acceptance that did not occur.

### Payment status and the remaining handoff gap

The billing read does not add a payment-method write or a secure one-time cardholder
handoff. It returns `paymentAuthority.secureHandoff.available: false`. The reason is
`secure_handoff_unavailable`, or `card_collection_not_enabled` for an
organization outside the card-collection rollout. This field describes the
missing one-time ceremony, not availability of the existing capture-link tool.

Existing `save_billing` integrations keep the same `request`, `confirm` and
`status` actions, response shapes and permissions. `request` returns
`confirmation_required` with a confirmation token; `confirm` consumes that
token and returns `human_action_required` with the cardholder URL. That legacy
URL can be reopened until verification or expiry. It does not provide a
one-time browser exchange with fresh human proof, and is not the secure handoff
required to complete the new text-only setup flow. Do not describe that missing
capability as complete or ask for card details in chat.

For current payment readiness, poll `get` with `{ "kind": "billing" }` every
15–30 seconds. Its `paymentAuthority` fields distinguish current verification
from setup history. `status` and `expiresAt` describe the latest capture link,
at the same path as the `save_billing` status poll:

| Evidence | Interpretation |
| - | - |
| `status: null` | No capture record exists; this does not prove a request was staged or confirmed |
| `status: "pending"` | A legacy request was confirmed and a capture was issued; no card is verified by this fact |
| `status: "opened"` | The capture was accessed; opening it is not fresh human proof or card verification |
| `status: "expired"` | The capture expired |
| `status: "verified"` | That capture was verified historically; the card may since have been removed |
| `submission.status` | `processing`, `verified`, `failed` or `expired` from the latest submitted secure card form; `null` means no current submitted attempt |
| `verified: true` | The canonical billing account currently has a verified payment method |

Existing `save_billing` calls with
`{ "paymentAuthority": { "action": "status" } }` keep their capture-history
response: `paymentAuthority` contains `method: "capture_link"`, `status` and
`expiresAt`, and the action is `payment_authority_pending`,
`payment_authority_opened`, `payment_authority_verified` or
`payment_authority_expired`. Existing card-collection permissions still apply;
no capture record returns `NOT_FOUND`. These responses do not report the current
payment method or submitted form state. Use the new billing read for those facts.

After verification, read billing readiness again. A verified payment method
does not imply every billing action is complete. Use the canonical next-action
explanation to distinguish good standing from a pending decision. This read
does not grant purchasing or product access. If the next action cannot be
completed in chat, explain that limitation and use its documentation link.
The new text-only readiness flow stops before card setup. A versioned one-time
handoff with fresh cardholder confirmation is still needed. The existing
capture-link contract continues to serve deployed clients.

Legal acceptance and payout changes remain authorization-sensitive. The three
named Plan & Billing tools in this table are private Page capabilities and may
be omitted from model context. Follow the tool response and Page requirements;
never pass card or bank details through free text.

## Buyer tools

| Tool | Purpose |
| - | - |
| `save_advertiser` | Create or patch an advertiser, including inherited tracking defaults; archive or restore one advertiser (`advertiserId`) or several in one call (`advertiserIds`) |
| `save_buyer_operator` | Confirm the buyer account's commercial operator identity used with sellers; this does not grant login access |
| `save_campaign` | Create or patch a campaign, including a per-seller MediaBuy frequency cap, local trackers, and inherited-tracker overrides; set the full set of creatives on it with `creativeIds`; pause, archive, restore, reactivate, explicitly launch, or request asynchronous cancellation of it and every nonterminal media buy |
| `save_dimension` | Create or update a buyer-owned dimension and its values; labels remain fields on advertisers and campaigns |
| `upload_creative_asset` | Open the private upload Task for one buyer-owned advertiser; no destination connection or provider account is required |
| `save_creative` | Create, update, or archive a creative; use `sourceAssetRef` for one finalized image or `sourceAssets` with the selected `creativeFormatId` to bind finalized images and promoted MP4s to canonical format slots; set the full set of campaigns an advertiser creative runs on with `campaignIds` |
| `save_creative_collection` | Create or change a non-executable advertiser or campaign collection; advertiser writes use a revision and saved-creative members only |
| `save_audience` | Sync first-party CRM audiences for an advertiser; add, remove, or delete an audience and then read its match status |
| `save_event_source` | Create, change, archive, or restore up to 50 conversion event sources (pixels, server feeds) for an advertiser in one call; each source succeeds or fails on its own |
| `request_proposals` | Ask every seller currently eligible for the buyer for proposals or products against one campaign revision. The campaign must target at least one country with `targetingOverlay.geo_countries` (directly, or through `targetingOverlay.geo_regions`, `targetingOverlay.geo_metros`, or a postal area); a campaign with none is refused before any seller is contacted. When the campaign has channel groups, only sellers whose declared channels can match a group are contacted; the others are listed in `skippedSellers`, and returned products outside every group are dropped. When the advertiser has an inclusion property list, the response provides bounded property coverage and missing-property reasons; partial seller evaluation is marked incomplete rather than presented as exact coverage. |
| `save_media_buy` | Accept a proposal, stage returned products, update a draft media buy (flight, budget, bid, pricing option, package performance standards, line items by `lineItemRef`), or archive an unwanted draft; repeat a product as an additional line item with `products[].selectionId`; change a live buy's budget or flight, or a running buy's end date (resend its current `startAt`) (`pending_approval`, with the change in `mediaBuy.pendingChange`, while it is not live yet); pause or resume one buy with `isPaused` (`pause_requested` / `resume_requested` while the seller is still applying it) |
| `get_delivery` | Query bounded Buyer campaign delivery |

Buyer `search` and `get` kinds include the account's advertisers, campaigns,
creatives, creative formats, creative collections, wholesale products,
proposals, media buys, sellers, asks, and organization-owned Agents, plus other
kinds advertised by the live schema. Follow the [Buyer
workflows](/v2/setup/v3/buyer-workflows) page for sequencing.

### Set package performance standards

Pass `performanceStandards` on a `save_media_buy` product when the buyer needs
a package-level commitment. It is a non-empty list. Each entry has one metric
(`viewability`, `ivt`, `completion_rate`, `brand_safety`, or
`attention_score`) and a `threshold` from 0 through 1. Viewability also
requires `standard: "MRC"` or `standard: "GroupM"`; do not send `standard` for
the other metrics. IVT is a maximum. Every other metric is a minimum.

You may name a measurement vendor with
`vendor: { "domain": "vendor.example", "brand_id": "optional-id" }`. The
vendor must be a brand reference: `domain` is required and `brand_id` is
optional.

Only send these terms for a product or seller that declared performance-standard
support in discovery. At launch, Interchange forwards the exact standards when
that support is present. Otherwise it refuses the launch with
`SELLER_PERFORMANCE_STANDARDS_UNSUPPORTED`; it never omits the terms silently.
Use `get(kind: "media_buy", include: ["products"])` to read the accepted
standards from each package.

```json theme={null}
{
  "products": [
    {
      "productId": "qualified-product-id",
      "performanceStandards": [
        {
          "metric": "viewability",
          "threshold": 0.7,
          "standard": "MRC",
          "vendor": { "domain": "measurement.example" }
        },
        { "metric": "ivt", "threshold": 0.02 }
      ]
    }
  ]
}
```

### Sync a first-party audience

Use `save_audience` to send a batch of first-party CRM audience changes for an
advertiser that the active Buyer Account can write. Each item has a buyer-owned
`audienceId` and can add members, remove members by `external_id`, or delete the
audience entirely. Deleting an audience is destructive.

Members are AdCP `AudienceMember` objects. Provide each added member's
`external_id` and either a SHA-256 `hashed_email`, a SHA-256 `hashed_phone`, or
an `email` that the service normalizes and hashes before forwarding. Hashes are
lowercase hex. If both `email` and `hashed_email` are present, they must
represent the same normalized email. Do not put raw PII in `external_id`.

```json theme={null}
{
  "advertiserId": "ADVERTISER_ID",
  "audiences": [
    {
      "audienceId": "loyal-customers",
      "name": "Loyal customers",
      "consentBasis": "consent",
      "add": [
        {
          "external_id": "crm-123",
          "hashed_email": "SHA256_OF_NORMALIZED_EMAIL"
        }
      ]
    }
  ]
}
```

A successful save returns `action: "synced"`, an `operationId`, the
`advertiserId`, and `audienceCount`. The operation is asynchronous. Read the
advertiser's current audience status and uploaded and matched counts with
`get({ kind: "audience", advertiserId: "ADVERTISER_ID" })`; there is no
separate audience-status tool. Each audience reads back under the names
`save_audience` takes (`audienceId`, `name`, `consentBasis`), with `status`,
`uploadedCount`, `matchedCount`, `lastOperationStatus`, `createdAt`, and
`updatedAt` beside them. The V3 audience workflow does not support the
V2 missing-audience sweep or push-notification callback. To remove an audience,
send that audience's `audienceId` with `delete: true`.

### Save event sources

Use `save_event_source` to register the conversion event sources (pixels,
server feeds, CRM or warehouse syncs) that an advertiser sends events from.
One call covers one advertiser and up to 50 sources. Each entry in
`eventSources` is keyed by your own `eventSourceId`: a new id creates the
source, an existing id updates it. `name` is required when an entry creates a
source; Interchange never fills it in for you.

```json theme={null}
{
  "advertiserId": "ADVERTISER_ID",
  "idempotencyKey": "sync-ADVERTISER_ID-2026-09-29-001",
  "eventSources": [
    {
      "eventSourceId": "purchase-completed",
      "name": "Purchase Completed",
      "eventTypes": ["purchase"],
      "actionSource": "website",
      "valueCurrencies": ["USD"]
    },
    { "eventSourceId": "legacy-pixel", "isArchived": true }
  ]
}
```

To say which pixel a source uses on a seller account, add `sellerAccounts`
to its entry: up to 20 pairs of `accountId`, the seller account's
`accounts[].id` from `get({ kind: "connection", id: "CONNECTION_ID" })`, and
`sellerId`, the id of the pixel on that account (for example a Meta dataset or
a Snap Pixel). Name the pixel when the account has more than one.

```json theme={null}
{
  "eventSourceId": "purchase-completed",
  "sellerAccounts": [{ "accountId": "SELLER_ACCOUNT_ID", "sellerId": "PIXEL_ID" }]
}
```

Saving records the request; it does not contact the seller or check the
pixel. Interchange checks the pixel when the source syncs to the account,
which happens when you create a media buy on that account whose optimization
goal names the source, while the **Conversion events** switch is on for that
seller. This is rolling out account by account; see
[how a source reaches each seller account](/v2/guides/measurement-incrementality#how-a-source-reaches-each-seller-account).
Saving again with a different `sellerId` changes the pixel for that account.
If the account already uses a confirmed pixel, it keeps using that pixel
until Interchange confirms the new one. If the account's sync `failed`,
`expired`, or was `removed`, naming a pixel again returns it to `pending` so
it can be retried. A save that changes only an event source's seller
accounts reports `updated`.
Accounts you leave out of a later save keep their binding. An `accountId`
that is not an active, connected account of yours fails the entry with
`REFERENCE_NOT_FOUND`, and the source is not saved.

Send `isArchived: true` to archive a source, and `isArchived: false` to undo
the archive with its configuration kept. An entry that sets `isArchived`
carries no other change. Campaign optimization goals that reference an
archived source stop resolving it until the archive is undone.

The response has one entry per source you sent, in the same order, each with
its own `action`:

* `created`, `updated`, or `unchanged`: the full event source, plus `setup`,
  which links the guide for sending events. Undoing an archive reports
  `updated`.
* `archived`: the `eventSourceId` only.
* `failed`: the `eventSourceId` and `errors` in the AdCP error shape. Each
  error has a `code`, a `message`, a `recovery` hint, and, where it applies,
  the `field` at fault:
  * `VALIDATION_ERROR` (`correctable`): the entry is invalid, for example a
    new source sent without a `name`.
  * `REFERENCE_NOT_FOUND` (`correctable`): an entry that sets `isArchived`
    named an `eventSourceId` the advertiser does not have, or a
    `sellerAccounts` entry named an `accountId` that is not connected.
  * `SERVICE_UNAVAILABLE` (`transient`): the entry could not be saved; send it
    again with a new `idempotencyKey`.

One failed entry does not stop the others. The `idempotencyKey` covers the
whole call: repeating the same call with the same key returns the first
result, including any failures, with `replayed: true`. To retry a failed
entry, send it again with a new key.

Each event source reports `health` in the AdCP `EventSourceHealth` shape.
`health.status` is the AdCP grade (`insufficient`, `minimum`, `good`, or
`excellent`), `health.last_event_at` is when Interchange last received an
event from the source, and `health.issues` lists anything that needs
attention, such as no events received yet or a rejected event. Registering a
source does not prove that events arrive: read it with
`get({ kind: "event_source", id: "EVENT_SOURCE_ID", advertiserId: "ADVERTISER_ID" })`
before relying on it.

`sellerAccounts` lists the seller accounts the source is bound to. It is
empty until you name a seller account. Each entry has the account's
`storefront` (`id` and `name`), its `accountId`, `sellerId`, `status`,
`lastSyncedAt`, `health`, and, while the account cannot use the source yet, a
`blocker` with a `code` and `message`. `sellerId` is the pixel Interchange
has confirmed on the account. It stays `null`, and `status` stays `pending`,
until a sync confirms the pixel, even after you name one. A `pending` account
carries the `SELLER_OBJECT_SYNC_PENDING` blocker. The pixel you named is not
returned. When you change the pixel on an account that already has a
confirmed one, the account keeps using the confirmed pixel, which stays in
`sellerId`; it reads `status: "pending"` with the
`SELLER_OBJECT_SYNC_PENDING` blocker until Interchange confirms the new one.

`sellerAccounts[].health` is the seller's own assessment of the confirmed
pixel, in the AdCP `EventSourceHealth` shape, as the seller reported it when
Interchange confirmed the pixel. It is `null` until a pixel is confirmed, and
`null` when the seller gives no assessment. A pixel that is set up but has
not received events yet reads `events_received_24h: 0` with no
`last_event_at`; send events to it before relying on it for optimization.
Interchange returns it as the seller reported it. The source's top-level
`health` is Interchange's view of the events it received;
`sellerAccounts[].health` is the seller's view of its pixel.

A seller account that cannot take event sources at all reads
`status: "failed"` with the `SELLER_OBJECT_TYPE_UNSUPPORTED` blocker. These
states never block saving or launching a campaign; a campaign's
`optimizationGoals` name event sources by `eventSourceId`, and their state on
each seller account is read here.

On every buyer read, a `sellerId` identifies a Storefront, with one exception: on an event source, `sellerAccounts[].sellerId` follows AdCP's `seller_id` and is the seller's own ID for that event source, such as a Meta dataset or Snap Pixel ID. It is not an internal
customer ID. For one compatibility release, a seller's deprecated
`connection.customerId` remains available for existing integrations. Use
`sellerId` or `storefrontId` instead; `connection.customerId` will be removed
in a follow-up release after this transition period.

For a saved Creative, add `include: ["preview"]` to `get`. The read returns a
15-minute render URL bound to the exact current Creative state revision, or a reason
when the format has no first-party renderable asset. The URL runs only on the
isolated Apostra render origin. A changed Creative invalidates the old URL,
and the response never includes storage URLs, provider preview HTML, or
measurement trackers. Direct image, video, and audio source assets are
renderable; active HTML, VAST, DAAST, and ZIP documents return a reason instead.

Before assembling uploaded assets, search the selected advertiser's visible
canonical format declarations. This read does not require a campaign and does
not make a provider call or setup request. Seller-owned product declarations
are available without a provider connection; direct-adapter products keep
their existing active-connection visibility rules:

```json theme={null}
{
  "kind": "creative_format",
  "filter": {
    "advertiserId": "ADVERTISER_ID",
    "productId": "PRODUCT_ID"
  }
}
```

`productId` is required to keep the catalogue read bounded. Add `sellerId` or
`formatKind` when the selected product supplies that scope.

Each result returns an opaque `id`, the seller's display name and id, the exact
`formatOptionRef`, its canonical `formatKind`, any declared frame and size, and
its slots. Every slot includes its stable `slotId`, required flag, minimum and
maximum cardinality, accepted asset kind, and the exact authored constraint
fields supplied by the product. Use the opaque result `id` with
`get({kind: "creative_format", id, advertiserId})` for stable readback. The
read includes only currently buyer-visible product and publisher declarations;
it does not add entries from the legacy global format catalogue. Pass the
returned `id` as `creativeFormatId` when calling advertiser-scoped
`save_creative`. The server revalidates the selected advertiser, seller,
product route, option, and declaration before reading an asset or writing a
Creative. If `formatKind` or `formatOptionRef` is also supplied, it must match
the selected format.

Search creative collections with exactly one campaign or advertiser scope. You can
create an advertiser collection without attaching it to a campaign. To read one,
use `get` with `kind: 'creative_collection'`, its id, and `advertiserId`. To change
an advertiser collection, use `save_creative_collection` with `advertiserId`,
`collectionId`, and the returned `updatedAt` as `expectedUpdatedAt`. Updates allow
name, description (`description: null` clears it), or `parentId` (`parentId:
null` clears it); member changes accept saved creative ids only. Results include
`parentId` and `descendantMemberCount`, which counts distinct members across the
collection and its active descendants. A parent must belong to the same
advertiser, cannot be the collection or one of its descendants, and cannot make
the hierarchy deeper than 16 levels. Archive with `isArchived: true` only after
archiving child collections. Restore with `isArchived: false` and the archive
response's `expectedUpdatedAt`; a collection with a parent can be restored only
while that parent is active and belongs to the same advertiser. A stale revision
does not change the collection, and `expectedUpdatedAt` is refused on campaign
collections, which carry no revision fence. Advertiser collections remain
non-executable: they do not attach campaigns or set roles, sync policy, or
arbitrary metadata.

### Replay an uncertain advertiser create

When creating an advertiser, you may supply an `idempotencyKey` of 16–255
letters, numbers, underscores, periods, colons, or hyphens. Use one key for one
logical create and reuse that exact key with the unchanged create payload if the
response is lost or uncertain. The replay returns the originally created
advertiser; changing the payload with the same key returns a conflict. The key
is create-only and optional, so a create without it retains ordinary create
behavior. Keep `correlationId` unique for each transport attempt: it is a
diagnostic handle, not the replay key.

### Create and verify a campaign creative

Call `save_creative` with a `campaignId`, `name`, and optional `assets` to add a
creative directly to a campaign. Each asset supplies exactly one public `url`
or base64 `dataUrl`, plus its `assetType`. The direct tool accepts image, video,
audio, HTML, VAST XML, and text media; ZIP bundles continue through the inspected
[HTML5 upload flow](/v2/object-guides/creative#html5-bundles). Add `clickUrl`
when the creative has a click-through destination. `makePrimary` is available
for image, video, audio, HTML, and VAST assets; text can be attached only as a
subsidiary asset.

For a local JPEG, PNG, or MP4, select the owning `advertiserId` and call
`upload_creative_asset`. A destination Storefront connection and provider
account are deliberately not part of this V3 source identity.
The Task hashes the file in the browser and sends the bytes directly to private
storage. File bytes never enter model or chat context. The signed PUT URL is
returned only to the embedded Task app; it is excluded from model-visible
results, chat/activity, logs, and persistence.

Configured buyer ingress accepts JPEG and PNG files of 1–30,000,000 bytes.
When managed-video ingress and inspection are configured, authenticated Buyer
Accounts also accept MP4 files of 1–2,000,000,000 bytes.
Finalization verifies the declared MIME type, exact size, and digest. MP4
finalization also reports asynchronous media inspection state. An expired
preparation must be started again; retrying a completed upload returns the same
source identity.

Pass a finalized JPEG/PNG source reference verbatim as `sourceAssetRef` to
`save_creative`. The advertiser is the supplied `advertiserId`, or is derived
from `campaignId`.

For an eligible reference, use `sourceAssetRef` instead of `assets`:

```json theme={null}
{
  "campaignId": "CAMPAIGN_ID",
  "name": "Autumn hero",
  "sourceAssetRef": "scope3-asset://v1/123e4567-e89b-42d3-a456-426614174000",
  "clickUrl": "https://example.com/autumn"
}
```

The server accepts only the customer, authenticated principal, and advertiser
that owned the finalized upload. It re-verifies the private bytes, copies them
into the governed creative asset store, and saves only the durable asset URL
plus a one-way source fingerprint. Private object paths, source references,
and signed URLs are never persisted on the creative.

With `campaignId`, the creative is created under that campaign's advertiser
first and then attached through normal campaign membership. Retry the same
`sourceAssetRef` and `name` after an uncertain response: an existing adoption
is returned and any interrupted campaign attachment is completed. A different
name is refused rather than renaming or duplicating the adoption. Use
`advertiserId` instead of `campaignId` to keep the creative in the advertiser
library for later assignment.

To assemble uploaded assets before a campaign exists, search the selected
product with `search({kind: "creative_format", filter: {advertiserId,
productId}})`, then pass the result's opaque `id` as `creativeFormatId`. Bind
each durable asset to a declared slot through `sourceAssets[].assetId`. The
upload Task does not choose a format. `save_creative` derives the canonical
`formatKind` and `formatOptionRef` from the signed selection and resolves only
its exact seller and product route.

Use `advertiserId` with `creativeFormatId` to keep the assembled Creative in
the Library without a campaign. For a campaign-scoped create, use `campaignId`,
`formatKind`, and the campaign product's exact `formatOptionRef` instead; the
save also attaches the new Creative to that campaign. V3 does not yet attach an
existing advertiser-only Creative; use the V2
`attach_creatives_to_campaign` operation for that later step. Ordinary
`assets` are unbound and
cannot satisfy those slots in the same call. Repeatable groups use
`group[index].member`: bind uploaded media through `sourceAssets[].slot` and
text or URL members through `social.components[].slot`, using the same
zero-based index for every required member in an instance. Indices must be
contiguous from zero. For example, a hosted-video format can bind an MP4 to
`video` and a JPEG or PNG to `thumbnail`:

```json theme={null}
{
  "advertiserId": "ADVERTISER_ID",
  "name": "Autumn video",
  "creativeFormatId": "SIGNED_CREATIVE_FORMAT_ID",
  "sourceAssets": [
    {
      "assetId": "123e4567-e89b-42d3-a456-426614174000",
      "slot": "video",
      "makePrimary": true
    },
    {
      "assetId": "223e4567-e89b-42d3-a456-426614174000",
      "slot": "thumbnail"
    }
  ]
}
```

To save an incomplete assembly, add `"mode": "draft"`. A draft is scoped to
the advertiser and can omit required media, copy, destination, or repeat-group
slots. The response and every later Creative read return its `state` and
whole-state `stateRevision`. The existing opaque `revision` remains the
buyer-preview projection digest.

Revise the same draft by sending its `creativeId`, current `stateRevision` as
`expectedRevision`, the signed `creativeFormatId`, and the complete replacement
`sourceAssets` and authored-slot state. The save replaces the prior submitted
state; it never merges two revisions. A stale revision fails without changing
the Creative.
Use `"mode": "complete"` on that same replacement when all required slots are
present. Completion runs the format's current server-side validation and keeps
the Creative in the advertiser Library without attaching it to a campaign or
contacting a provider. Existing calls that omit `mode` keep complete-save
behavior.

The server re-proves every source against the advertiser. A video must have
completed inspection and reached `promoted`. Saving under `advertiserId`
creates the canonical Creative without a campaign or provider call. Saving
under `campaignId` also attaches it. Video delivery remains
`destination_required` until V2 `attach_creatives_to_campaign` resolves a
compatible selected Meta destination. That governed attachment rechecks the
durable Library source and current account authorization, then uses normal Meta
review and readback. Other destinations remain unsupported for this bridge. Do
not substitute the provider-bound V2 adoption operation for a buyer-owned V3
source.

After the campaign executes, call `get` with `kind: "creative"`, `sourceId` set
to the campaign ID, and `id` set to the returned creative ID. When a legacy
Sales Agent library receives the creative through `sync_creatives`,
`platformLinks` rows show the
downstream ad-platform sync status, platform ID, approval status, and last sync
time. Inline package delivery does not create a `platformLinks` row. Each agent
has one authoritative row: the most recently updated account lineage wins
after a credential or account rotation. A platform ID confirms that the
destination created its creative object; the status and approval fields still
determine whether it is ready to deliver. Existing metadata-only
`save_creative` calls require no changes.

Start with `get_status`. Its `operatorIdentity.usableForBuying` field is false
when the operator is missing or still uses a platform-owned legacy fallback.
In that state, an account administrator must call `save_buyer_operator` with
the buyer's real non-platform domain and choose `whole_operator` or a stable
`specific_unit` before new discovery or buying work. When the domain is usable,
`scopeStatus` is `unclassified`, and `locked` is false, reuse that domain and
choose its scope before new AdCP 3.2 provisioning. If the identity is locked,
follow the support action from `get_status` instead. This confirms the
commercial operator identity used with sellers; it does not add users, change
account membership, or change the login organization.

### Launch a campaign with explicit confirmation

Launching with `save_campaign` is a two-call operation. First request a
preview without changing the campaign:

```json theme={null}
{
  "campaignId": "CAMPAIGN_ID",
  "desiredPhase": "active",
  "idempotencyKey": "campaign-launch-preview-v1"
}
```

This returns `action: "pending_confirmation"` — a successful, structured
response, not an error — with `campaign.revision`, the campaign budget and
flight, and under `launch` exactly what would go live: `mediaBuys` (each
staged media buy's id, name, phase, budget, and seller; the first 25, with
`mediaBuysTruncated: true` if there are more), `mediaBuyCount`, and
`combinedBudget` (the sum across those buys when they share a currency;
`null` with a per-currency `budgetsByCurrency` list when they do not).
`nextStep` carries the exact follow-up call, including a fresh `idempotencyKey` for the confirmation (the preview's key must not be reused, or the preview is replayed). It does not launch the campaign.
Do not combine this preview with campaign field changes; save those changes in
a separate call before requesting the preview.

After reviewing the preview, send that `campaign.revision` as
`expectedRevision` with explicit confirmation:

```json theme={null}
{
  "campaignId": "CAMPAIGN_ID",
  "desiredPhase": "active",
  "confirmLaunch": true,
  "expectedRevision": 7,
  "idempotencyKey": "campaign-launch-confirm-v1"
}
```

`confirmLaunch: true` requires the `expectedRevision` returned by the preview.
The server checks that revision again while holding the campaign mutation lock,
before any provider dispatch. If the campaign changed after the preview, the
call returns `REVISION_CONFLICT`; re-read the campaign, request a new preview,
and confirm that new revision instead of reusing the earlier confirmation.

### Cancel a campaign and its media buys

To cancel a campaign, send `desiredPhase: "canceled"` by itself with the
campaign's current `expectedRevision`:

```json theme={null}
{
  "campaignId": "CAMPAIGN_ID",
  "desiredPhase": "canceled",
  "expectedRevision": 7,
  "idempotencyKey": "campaign-cancel-v1"
}
```

This requests cancellation for every nonterminal media buy through its existing
seller path. It is asynchronous: a requested cancellation can return per-buy
`canceled`, `cancel_pending`, and `cancel_failed` outcomes; a buy already
terminal retains its observed terminal phase. While any buy is pending or
failed, the campaign reads as `phase: "ending"` with reason
`"canceled"`; the response plainly says it is not canceled until every buy is
confirmed. Pending exposure stays visible and releases only when that buy
becomes terminal. The background status sync moves the parent to
`phase: "canceled"` after the final child settles, without another buyer call.
Retry with the latest `expectedRevision` to re-drive pending or failed buys;
confirmed buys are not canceled again.
Archive is separate: use `isArchived: true` only when you want to remove a
campaign from default visibility.

### Archive and restore

`save_advertiser` and `save_campaign` archive with `isArchived: true` and
restore with `isArchived: false`. Archiving is a soft delete: the record moves
to archived state and everything under it is retained and stays readable.
Archiving an advertiser does not lock its campaigns, media buys, or creatives
— `search` and `get` on those children keep working — and it frees the
advertiser's name for a new advertiser. Restoring an advertiser whose name is
now held by an active advertiser returns `CONFLICT`; rename or archive the
other advertiser first.

To archive several advertisers in one confirmed action, pass `advertiserIds`
(up to 50) with `isArchived` instead of `advertiserId`. Each advertiser is
archived or restored independently and the response lists every outcome under
`results`; `partialWrite: true` marks a mixed result.

A restored campaign comes back as a draft: its media buys were retired when it
was archived, so stage inventory again before launching. `save_media_buy`
archives a **draft** media buy only (`isArchived: true`); a dispatched buy is
cancelled through the campaign or the v2 update contract, because that
cancellation may wait on seller or operator approval and must not be reported
as done before it is. An archived media buy cannot be restored; stage a new one
on the campaign.

### Advertiser and campaign tracking

Tracking is a typed field on the existing `advertiser` and `campaign` nouns;
it is not a separate V3 noun or tool family. V3 uses the same tracking services
as the V2 REST compatibility surface.

Set advertiser defaults with `save_advertiser`. This HappyDemics example keeps
the raw vendor tag, identifies its documented dialect, and lets the server
compile it to AdCP universal macros:

```json theme={null}
{
  "advertiserId": "ADVERTISER_ID",
  "tracking": {
    "enabledMacros": ["DEVICE_ID", "GDPR_CONSENT"],
    "trackers": [
      {
        "name": "HappyDemics exposure",
        "vendorName": "HappyDemics",
        "url": "https://c.ahdmw.com/pxl/EXPOSURE_ID?gdpr_consent={{GDPR_CONSENT_STRING}}&udid={{USER_ID}}",
        "trackerType": "impression",
        "sourceDialect": "happydemics"
      }
    ]
  }
}
```

Read it back with:

```json theme={null}
{
  "kind": "advertiser",
  "id": "ADVERTISER_ID",
  "include": ["tracking"]
}
```

The advertiser's `tracking.trackers[]` returns a stable `trackerId`, the
`url` as you saved it, the translated `canonicalUrl`, and the auditable mapping
metadata. In the example, documented HappyDemics tokens canonicalize as
`{{USER_ID}}` → `{DEVICE_ID}` and `{{GDPR_CONSENT_STRING}}` →
`{GDPR_CONSENT}`. Other double-brace spellings are not inferred without their
own vendor evidence.
V3 returns bounded mapping, diagnostic, and documentation summaries with their
total counts; the V2 tracking-config response retains the complete stored
mapping artifact for non-model audit clients. Long URL values use bounded
previews with their original length and a truncation flag, and effective
campaign entries do not duplicate the source tracker's mapping artifact. When
updating an existing tracker, send its `trackerId` and omit `url` to preserve
the complete stored URL; a returned truncated preview is also recognized and
will not overwrite that stored value. Send the returned advertiser `revision`
or campaign `campaignRevision` as `tracking.expectedRevision` to make any
tracking update an atomic compare-and-set; URL-preserving updates add that
guard automatically.

Tracker URLs are limited to 8,192 characters and 64 macro tokens. For a custom
event, set `trackerType` to `custom` and supply `customEventName`; that field is
invalid for impression or click trackers. Stored free-form tracking values
returned by V3 are bounded and fenced as untrusted external data. Tracker
labels, custom event names, and custom-macro descriptions can be passed back
unchanged on an update; the server restores their exact stored values. New or
changed custom-macro descriptions are limited to 240 characters.

Before saving or trafficking a tracker, call `test_creative_macros`. The tool
does not fetch the URL or change a creative. Standard request auditing retains
only statuses and counts, never tracker URLs or binding values. The tool
returns four independently
copyable stages—exact raw input, canonical AdCP form, recipient form, and final
synthetic substitution—plus mapping evidence, bindings, and diagnostics.

```json theme={null}
{
  "rawInput": "https://c.ahdmw.com/pxl/EXPOSURE_ID?udid={{USER_ID}}",
  "sourceDialect": "happydemics",
  "targetDialect": "gam",
  "scenario": "device_id_present",
  "scenarioKey": "review-1"
}
```

For this example, the stages show `{{USER_ID}}` → `{DEVICE_ID}` →
`%%ADVERTISING_IDENTIFIER_PLAIN%%`, while the final stage uses a visibly
synthetic advertising identifier.

For consent syntax inspection, `gvlVendorId` can show the corresponding GAM
`${GDPR_CONSENT_<id>}` form, but a caller-supplied number is not verification:
the report remains failed and not preview-ready. Without the number, the
consent parameter is removed and the report also fails closed. A production
consent mapping needs separate evidence that the ID belongs to the measurement
vendor; the tester never infers that relationship from a pixel URL.

Available scenarios are `device_id_present`, `device_id_unavailable`,
`gdpr_applies_with_consent`, `gdpr_does_not_apply`, and
`missing_required_value`. The unavailable-device scenario deliberately shows
an empty device value and labels it `privacy_suppressed`. Change `scenarioKey`
to obtain a different deterministic cachebuster. Explicit `values` must be raw
synthetic test values, never real user identifiers and never pre-encoded; a
value that already contains percent-encoded octets is flagged as possible
double encoding. Binding values and stage templates in the report are bounded
by UTF-8 bytes, and `projection` reports any truncation or omitted detail. The
whole Murph result stays within a 96 KiB budget. Inspect the substituted stage
for placement and encoding. Explicitly required macros that are absent, missing
required values, unknown or wrong-case tokens, ambiguous vendor syntax, unsafe
nesting, and missing recipient mappings keep `passed` and `readyForPreview`
false.

Campaigns inherit advertiser trackers dynamically. Add a campaign-local
tracker or disable one inherited default with `save_campaign`:

```json theme={null}
{
  "campaignId": "CAMPAIGN_ID",
  "tracking": {
    "trackers": [
      {
        "name": "Campaign measurement",
        "url": "https://measurement.example/pixel?cb={CACHEBUSTER}",
        "trackerType": "impression"
      }
    ],
    "overrides": [
      {
        "trackerId": "trk_0123456789abcdef01234567",
        "enabled": false
      }
    ]
  }
}
```

Request `include: ["tracking"]` when reading a campaign. Its `tracking` field
contains the local `trackers`, inherited `overrides`, and `effectiveTrackers`
with `sourceScope`, `inherited`, `effectiveEnabled`, and `statusReason`.
Within `tracking`, each supplied array is the complete desired array for that
scope; omit an array to leave that part unchanged.

## Seller tools

| Tool | Purpose |
| - | - |
| `save_seller` | Patch Seller identity, setup intent, declared capabilities, and the listing (description, coverage, subtitle, and website, support, privacy and terms links) |
| `save_inventory_source` | Create or update an inventory source, including one existing modular source module's non-secret configuration, without model-visible credentials |
| `save_material` | Register immutable seller source revisions from URLs, uploads, manifests, inline text, or history; reprocess lossless renditions; update metadata, including the seller-governed Library `documentPurpose`; or decide Material-owned candidates. A Material with no stored `documentPurpose` is returned as `uncategorized`; when updating metadata, omit the field to retain the existing category or set it to `null` to clear it. The closed purpose set does not include rate cards or pricing. The `mark_reusable` action sets one non-priced slide, page, or sheet `unitId` reusable; `UNIT_NOT_REUSABLE_KIND` rejects a document container and `UNIT_CONTAINS_PRICING` rejects a unit with commercial figures. This action is gated by the `sales-library` rollout. |
| `save_rfp` | Create or revise an RFP turn (including `imported` origin); record free-form feedback with optional typed `grade`, `ledBy`, and `commentary`; release a turn; attach an uploaded or historical-proposal response; endorse it with optional commentary; remove an endorsement; or request and cancel JSON, HTML, PDF, and PowerPoint proposal files. Response-pair actions are gated by the `sales-library` rollout. |
| `save_library_request` | Gated by the `sales-library` rollout. `open` creates or returns the one open request for the exact gap, optionally naming its origin RFP turn. `close` records uploaded or dictated seller Material, named by `closingMaterialId` as every read returns it, as the closing evidence. |
| `save_coverage` | Add or remove declared publisher domains and property claims |
| `save_wholesale_product` | Create or update a wholesale product |
| `save_media_kit` | Deprecated compatibility write for the legacy Business Profile; new clients author the buyer-visible listing through `save_seller`'s `listing` field (`mediaKit` is a deprecated alias for the same field) |
| `save_playbook` | Save selling guidance and house-discount declarations |
| `save_business_rules` | Save acceptance and operating rules |
| `save_advertiser_instructions` | Save instructions scoped to a buyer or advertiser relationship |
| `save_signal` | Create or update a seller signal |
| `save_work_item` | Complete or update a typed item from the unified work queue |
| `get_delivery` | Query seller delivery or cumulative margin; integrated Media Companies can also select Buyer campaign delivery |
| `get_rfp_performance` | Query aggregate, privacy-protected RFP quality, efficiency, and commercial metrics |

Seller `search` and `get` kinds include the Seller singleton, inventory sources,
material, RFPs and RFP turns, library requests, coverage, wholesale products, playbook, business rules, media buys,
work items, signals, asks, organization-owned Agents, and other kinds advertised by the live schema. Follow the
[Seller workflows](/v2/setup/v3/seller-workflows) page for sequencing.

### Inventory-source recorded health evidence

Read one source with `get({"kind":"inventory_source","id":"SOURCE_ID",
"include":["diagnostics"]})`. Its compact `health` projection preserves the
recorded `lastErrorAt`, `lastSuccessAt`, and `lastCheckedAt` values from source
diagnostics, including `null`. These are recorded observations, not a new
provider probe made by `get`: when the check timestamp is `null`, freshness is
unavailable rather than assumed to be now. A later recorded success makes an
earlier recorded error historical evidence; it does not erase the original
evidence or prove a transaction succeeded. A successful health observation is
not proof that a media buy was accepted or delivered. To request a new
no-spend check, use the existing `refresh_inventory_source_health` flow.

`search({"kind":"library_request"})` and
`get({"kind":"library_request","id":"LIBRARY_REQUEST_ID"})` are gated by
the `sales-library` rollout. Search accepts `filter.status` (`open` or
`closed`) and `filter.originRfpTurnId`; get returns the request's gap, status,
origin turn, and closing Material evidence.

### Organization-owned Agents

`search({"kind":"agent"})` lists the software Agents the active account's
organization owns. Add `query` to match an Agent ID, Agent display name, or
operator display name. This explicit-only kind does not appear in unrelated
broad object searches and does not turn a buyer counterparty Storefront into an
Agent.

Read one result with `get({"kind":"agent","id":"AGENT_ID"})`. Add
`include:["certification"]` for the complete customer-safe certification
summary, `include:["validationRuns"]` for up to five server-bound run summaries,
or `include:["diagnostics"]` for observed implementation health and the current
production-revision label. A returned run's `diagnosticRead` shows the exact
follow-up: pass its `validationRunId` with `include:["diagnostics"]` to receive
the bounded Activity graph. Sales Agent detail also names its immutable
versioned public `validationSkill`. These reads intentionally omit
credentials, private evidence, operator identifiers, relationship identities,
and owner-only action URLs.

Interactive seller Pages that run inside a host use the account-resolved v3
surface for these reads and writes. In Apostra's hosted bridge, the nested
`/mcp/:toolName` proxy accepts `surface: "v3"` and forwards the call to
`/mcp/v3`; callers still use the tool's canonical arguments, not REST paths.

The [Seller RFP lifecycle](/v2/setup/v3/rfp-lifecycle) guide defines the
`save_rfp` action union, polling states, complete response requirements, and
clarification/failure behavior.

The [RFP performance](/v2/setup/v3/rfp-performance) guide defines aggregate
metric formulas, valid dimensions, disclosure thresholds, and immutable
pagination. Individual RFP facts remain on `get({ kind: "rfp" })`.

Material is evidence and teaching input. It does not replace inventory sources,
products, pricing, Playbook instructions, business rules, signals, formats, or
Seller identity. Use
`get({ "kind": "material", "id": "MATERIAL_ID" })` to inspect the current
source revision, ingestion state, diagnostics, typed candidates, confirmation
receipts, bounded rendition summaries, and source artifact references. Explicit
includes expose independently paginated rendition blocks, reusable visual
assets, extraction diagnostics, composition receipts, and expiring source
access; originals and derived binaries are never embedded in the response.
Confirm a candidate by calling the exact `proposedMutation.tool` with its
arguments. Material prose is cited directly and is not a candidate to accept,
reject, or correct. Source records may carry supplied original and
derived structure references such as page or slide order, geometry,
table/image/chart assets, OCR/caption artifacts, digests, provenance,
confidentiality, and reuse rights. Completeness is reported per extraction
facet, and unsupported or degraded parsing is never presented as ready. See
[Seller workflows](/v2/setup/v3/seller-workflows) for upload finalization,
selection, retention, and access behavior.

### Material search filters

`search({"kind":"material"})` can narrow Library browse rows before paging.
The returned `total`, `nextCursor`, and `hasMore` describe that filtered result
set, not the unfiltered Library.

| Field | Accepted values | Behavior |
| - | - | - |
| `filter.documentPurpose` | `sales_deck`, `one_sheet`, `case_study`, `response`, `specification_sheet`, `uncategorized` | Filters by the seller-governed Library category. `uncategorized` also matches documents with no stored category. |
| `filter.sourceExtension` | A lowercase source extension, such as `pdf` or `pptx` | Filters by the source file type shown in the Library. |

## MCP App and resource-bound tools

These tools support interactive Pages or resources. A compliant host may keep
some of them out of model context and invoke them only through the associated
app contract.

The Product Marketing Page receives a session-bound capability from
`open_page`. It uses the host-only `teach_search_materials`,
`teach_get_material`, `teach_save_material`, and `teach_renew_capability`
aliases for Material listing and exact reads, seller-private URL and file
additions, upload finalization, Material-owned candidate decisions, and
capability renewal. MCP clients continue to use the public `search`, `get`, and
`save_material` tools. The bridge does not permit generic Material writes or
return source-access or visual-preview URLs. Expiring upload credentials are
returned only for the transient browser `PUT` step.

An active, unexpired Demo can also run the fixed synthetic evaluation from the
Product Marketing Page. These app-only tools require the same session-bound Teach capability
and cannot be invoked as general chat actions:

| Tool | Input | Result and limit |
| - | - | - |
| `teach_get_evaluation_status` | `{}` | Returns the current `materials_not_loaded`, `review_candidates`, or `ready` state, durable material and pending-candidate counts, and the synthetic-use disclosure. |
| `teach_prepare_synthetic_materials` | `{ clientRequestId }` | Prepares the fixed six-source synthetic teaching set. An `incomplete` response lists material IDs already saved; retrying continues the same generation. |
| `teach_start_synthetic_evaluation` | `{ clientRequestId }` | After every candidate is reviewed, starts the server-selected held-out brief and returns its exact evaluation RFP and turn. The caller cannot supply or replace the brief. |

All results are rehearsal-only. Synthetic rate-card figures are planning
examples, not live prices or buyer-quotable rates. Custom fulfilment requires a
named human or Connect follow-up; the evaluation does not make it automated or
available now. See [Controlled synthetic evaluation](/v2/storefront/product-marketing/overview#controlled-synthetic-evaluation)
for the full seller workflow.

The Library Page uses the host-only `library_*` aliases the same way, including
`library_get_rfp_turn` and `library_open_proposal_pass` for opening the
authorised Proposal Pass of a response from the Usage view; see
[MCP Apps](/v2/reference/mcp-apps#page-capability-and-tool-bridge) for the
full list.

| Tool | Account | Purpose |
| - | - | - |
| `create_feed_modular_inventory_source` | Seller | Open the modular-source creation Page |
| `seller_approvals_page` | Seller | Operate approval actions that remain interactive |
| `get_modular_inventory_source_readiness` | Seller | Read resource-bound modular readiness |
| `prepare_modular_source` | Seller | Prepare a modular source from its Page flow |
| `get_inventory_feed_context` | Seller | Read inventory-feed context |
| `preview_inventory_feed` | Seller | Validate and preview an inventory feed |
| `commit_inventory_feed` | Seller | Commit a reviewed inventory feed |
| `seller_notification_page` | Seller | Open Seller notification settings |
| `buyer_notification_page` | Buyer | Open Buyer notification settings |
| `sessions_get_brief_artifact` | Seller | Sessions Page-only read of the brief linked by a recorded RFP request event |
| `sessions_open_media_buy_timeline` | Seller | Sessions Page-only launch of the timeline linked by a `media_buy` downstream reference |
| `get_buyer_sellers_page` | Buyer | Temporary compatibility read for cached Media Partners Page bundles |
| `get_buyer_advertisers_page` | Buyer | Load the Advertisers Page's account landing projection |
| `buyer_sellers_page` | Buyer | Temporary compatibility write for cached Media Partners Page bundles |
| `open_add_advertiser` | Buyer | Open the advertiser setup Task from the Media Partners Page |
| `set_storefront_operator_domain` | Seller | Save the brand domain from the Listing page |
| `get_discovery_publication` | Seller | Read Discovery visibility, publish state, and OpenAI Apps verification status |
| `publish_discovery` | Seller | Set Discovery visibility to Apostra or Public and publish it (Public requires a verified public listing domain, gated by rollout). The Page's separate, confirmed Unlist action sends `private`, publishing a private revision that removes the storefront from Apostra and the public web; `private` is a state action, not a third distribution choice. |
| `save_discovery_hostname` | Seller | Save or replace the storefront's public listing domain |
| `activate_discovery_hostname` | Seller | Verify or re-verify the storefront's public listing domain |
| `download_discovery_printable_card` | Seller | Page-only browser download of a verified printable listing PDF from the current published revision |
| `update_discovery_openai_challenge` | Seller | Set or remove the OpenAI Apps verification token |
| `probe_discovery_openai_challenge` | Seller | Re-check OpenAI Apps verification status on demand |
| `get_chatgpt_app_config` | Seller | Read the ChatGPT app's listing projected from the seller's listing, hostname/DNS state, verification-token status, and submission-bundle availability |
| `save_chatgpt_app_listing` | Seller | Save the ChatGPT app's category and icons — name, subtitle, description, and every URL project read-only from the seller's listing |
| `save_chatgpt_app_hostname` | Seller | Save the ChatGPT app's custom hostname |
| `activate_chatgpt_app_hostname` | Seller | Verify DNS/TLS ownership and activate the ChatGPT app's hostname |
| `save_chatgpt_app_verification_token` | Seller | Set or remove the ChatGPT app's own OpenAI Apps verification token |
| `probe_chatgpt_app_verification_token` | Seller | Re-check the ChatGPT app's verification status on demand |
| `get_chatgpt_app_bundle_url` | Seller | Mint a fresh, short-lived signed submission-bundle download URL, run at click time |
| `approve_chatgpt_app_listing` | Seller | Record the app owner's explicit approval of the projected listing, and optionally acknowledge a legacy-listing reconciliation conflict |

The Buyer Page companion tools in this table are app-only and are not part of
the model-visible buyer catalog.
Current Media Partners Page bundles use public V3 `search`, `get`, `save_connection`,
and `save_ask`; the two closed Media Partners Page facades remain only for a rolling
deploy window. Current Campaigns bundles instead use the resource-bound
app-only `buyer_campaigns_page_search` and `buyer_campaigns_page_get` tools,
backed by canonical V3 projections; Advertisers uses its closed
landing-projection read. None of these Pages receives the generic `api_call`
surface.

Except for `download_discovery_printable_card`, the Listing and ChatGPT
destination tools above are shared between the model-visible seller catalog
and the Listing page (see [Seller Pages](/v2/setup/seller-pages)). They use
the same account, administrator, and confirmation checks in both paths. The
printable listing tool is app-only because it returns PDF bytes for a browser
download; print fulfillment and provider orders remain outside the tool.

## Core read examples

List objects:

```json theme={null}
{ "kind": "campaign", "query": "autumn", "limit": 25 }
```

Read one object with related detail:

```json theme={null}
{
  "kind": "campaign",
  "id": "CAMPAIGN_ID",
  "include": ["mediaBuys", "creatives", "tracking"]
}
```

Campaign `proposals`, `recentActivity`, `deliverySummary`, and `sourceIdentity`
are accepted include names so the API can return a precise
`unavailableIncludes` explanation, but they are not populated in the preview.

Search documentation:

```json theme={null}
{
  "query": "how does publisher authorization work?",
  "sources": ["docs"]
}
```

Read a selected documentation result by its returned `document` path:

```json theme={null}
{
  "query": "how does publisher authorization work?",
  "document": "storefront/inventory-sources/publisher-properties-coverage",
  "sources": ["docs"]
}
```

Arbitrary web URLs and internal repository paths are not readable through the
documentation search contract.

## Read structured results

Every tool returns MCP `content` for a person or model to read. Integrations
should make decisions from `structuredContent`. `tools/list` is authoritative
for inputs and publishes an output schema where the current tool declares one;
the patterns below cover the remaining preview tools.

| Tool family | Stable result pattern |
| - | - |
| `search` | A kind-specific object block with `results`, counts, `hasMore`, and an opaque `nextCursor` when another page exists |
| `get` | `{ kind, object }`, plus requested related data and `unavailableIncludes` when an accepted include cannot be populated |
| `save_<noun>` | `action` plus the saved noun or its ID; versioned nouns return the revision needed for the next guarded write |
| `request_proposals` | `executionId`, `status`, a full-cohort summary, one bounded `perSeller` page, `page.nextCursor`, and `skippedSellers` when channel groups left eligible sellers out; each seller carries an outcome, returned property coverage, Proposal IDs/a `productQueryId` and products, or a bounded error |
| `get_delivery` | Report rows, coverage and finality metadata, warnings, and pagination state |
| `get_rfp_performance` | Grouped metric datums, population, freshness, provenance, finality, disclosure semantics, and immutable pagination |

Do not parse IDs, revisions, actions, or recovery guidance out of rendered
prose. A write may return `action: "unchanged"`; that is a successful
idempotent outcome. On partial work, preserve `partialWrite`, `warnings`,
`errors`, per-seller or per-source outcomes, and any returned current revision.
`request_proposals.status` is normally `running` while durable background work
contacts the cohort; retry that same key until it becomes `complete`,
`partial`, or `failed`. A buyer can have only one running proposal execution
across campaigns. Terminal result cursors can continue a product-heavy outcome
for the same seller, and `detailsTruncated` marks byte-bounded product fields.
Each terminal seller result has one safe `outcome`: products returned, no
matching products, no inventory for the request, a skipped seller, or a failed
seller. `no_inventory_for_request` is a completed seller choice, not a failure
or retry instruction. `skipped` with `reasonCode` `advertiser_activation`
means the seller is off for the campaign's advertiser and was not contacted;
the seller's `error.message` says where to turn it on. It appears only for a
seller you named, through `sellerIds` or the campaign seller list. When
neither applies, `request_proposals` contacts only the eligible sellers that
are on for the campaign's advertiser; the others are not contacted or listed.
If none is on, the request fails with `NO_ELIGIBLE_SELLERS` and a message
saying where to turn a media partner on. `propertyCoverage` counts the property domains present in
returned products. When a seller reports filter accounting or unfinished work,
`filterResult` distinguishes no match, filtered-out inventory, and partial
results without exposing seller operational detail.
When a seller cannot execute an exact age range, its proposal remains available
as a counter-pitch and its semantic document names the limitation and the
seller's supported ranges. A direct `get_products` response carries the same
information in its additive `limitations` array: `code`, buyer-readable
`message`, requested `min`/`max`/`include_unknown`, and `supported_ranges`.

### AI-8599 bounded targeting continuation (cursor protocol v1)

Capability details can exceed a model-safe response page. V3 therefore exposes
two signed, opaque continuation fields; they are discovery/readback only and
do not contact a seller or start a new proposal round.

| Call | Field | Use |
| - | - | - |
| `request_proposals` | `capabilityCursor` (optional, maximum 4,096 characters) | Continue capability details from the same completed idempotent request. Reuse the original campaign, expected revision, and idempotency key; do not combine it with `resultCursor`. |
| `get({ kind: "proposal", include: ["products"] })` | `proposalProductsCursor` (optional, 1–2,048 characters) | Continue that Proposal's product/capability text at the same revision. This field is invalid for every other kind or without `include: ["products"]`. |

Cursors are account- and snapshot-bound. A forged, stale, or wrong-scope cursor
returns a validation error; a Proposal's expiry remains explicit in its
read-only `state` and never gives a cursor execution authority. Follow a
continuation only while the response supplies its next cursor. A terminal page
has `complete: true` and no next cursor. A capability marked unavailable or
blocked is not complete actionable targeting: demographic selections remain
rejected before seller dispatch while the application seller transport remains
on AdCP 3.1. If a signed continuation cannot fit its approved field limit, V3
omits the token and marks the capability unavailable rather than truncating its
scope or presenting omitted details as complete.

For example, a successful advertiser creation returns the new identity under
the noun rather than as an unlabelled string:

```json theme={null}
{
  "action": "created",
  "advertiser": {
    "advertiserId": "ADVERTISER_ID",
    "name": "Acme Europe",
    "primaryCurrency": "EUR"
  }
}
```

`request_proposals` returns enough state to continue each seller branch:

```json theme={null}
{
  "executionId": "EXECUTION_ID",
  "status": "partial",
  "perSeller": [
    {
      "sellerId": "10",
      "status": "quoted",
      "proposalIds": ["sfp1:QUALIFIED_PROPOSAL_ID"]
    },
    {
      "sellerId": "24",
      "status": "failed",
      "error": { "code": "UPSTREAM_ERROR", "message": "..." }
    }
  ],
  "summary": {
    "sellersRequested": 2,
    "sellersQuoted": 1,
    "sellersWithProducts": 0,
    "sellersResponded": 1,
    "sellersFailed": 1,
    "sellersPending": 0
  },
  "page": { "returned": 2, "total": 2, "hasMore": false }
}
```

These examples show the continuation fields, not every property. Treat the
live output schema as authoritative as the preview evolves.
Follow every `page.nextCursor` before treating the review as complete.

## Errors and result handling

Tool errors use bounded structured codes and explanatory text. Common classes
include validation errors, access denial, not found, revision conflict,
unsupported preview behavior, external failure, and partial results.

* Treat `isError: true` as a failed tool result even when the MCP transport
  itself returned successfully.
* Preserve structured IDs and recovery fields; do not parse prose to recover an
  identifier already present in `structuredContent`.
* A partial result is not an empty result. Read its warnings, coverage, and
  per-source or per-seller outcomes.
* Re-read after any response that reports a partial write.

For transport-wide behavior, see [Errors](/v2/reference/errors),
[Rate Limits](/v2/reference/rate-limits), and
[Preview limitations](/v2/setup/v3/limitations).


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