Skip to main content
tools/list is authoritative for the account kinds you can reach. A principal with both Buyer and Seller access receives their deduplicated union at connection time, while calls remain authorized against the account each call names with accountId, which every tool except get_status, save_account and save_directed_campaign_subscription requires. On those two writes accountId used to name the thing being written, so it stays optional until the old shape is removed; send it beside id or providerAccountId. See Name the account on every call. Enrollment, permissions, feature rollout, and host support can narrow the catalog. This page explains the complete registered families and intended use.save_lesson is available in a Seller Account context. Name the Seller Account with accountId. save_session can appear when a compatible Session source is available.
The Identity and brands guide 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 is retired on v3 and is in no tool list. A client working from a cached tool list can still call it for now; its result says it is retired and to send accountId on every call instead. Call get_status with no accountId to list the accounts you can reach, or with an accountId to read that account’s readiness and available tools.

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 or sellerAccountMappingId (never both) and optional view of media_buys, creatives, or delivery. With neither ID, it opens the Seller’s unfiltered Media Buys Page. sellerAccountMappingId is the ID of a company-owned seller account mapping, as the seller account reads return it. It is not a seller’s Account id. sellerAccountId was the earlier name of this filter. A call that still sends it is refused with a VALIDATION_ERROR that names sellerAccountMappingId, and the Page’s own data read takes only sellerAccountMappingId too. When an ID is supplied, the tool first proves that the relationship or seller account belongs to the authenticated Seller Account. An unknown or another seller’s ID fails closed: it returns no Page directive and opens nothing. The Page then shows only that buyer account’s activity with the Seller — its media buys and synced creatives — never the buyer’s own campaigns. A seller account focus lists the buys routed through that account; buys still routed under a legacy relationship appear under the relationship focus instead.

Open Seller Dashboard

open_seller_dashboard opens Seller Dashboard. It takes no arguments other than accountId and is listed only for Seller Accounts; a call from another account returns the error that names the Seller Accounts you can reach and tells you to name one with accountId, and opens nothing. The Page chooses its own view. While the Seller Account holds an active, unexpired 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 run report (activity and outcomes, win rate, the posture that converts best, what the agent learned, seasonality, strategy signals, recommended actions, top buyers and products, and recent runs) above the RFP metrics. Open a recent run to read its decision record. The server decides the report’s layout, the one insight it leads with and which optional panels have anything to show; the Page only draws them. The report and a run’s decision record come from two app-only reads, seller_dashboard_get_analytics and seller_dashboard_get_decision_record, which are not model-visible and refuse a call without the Page’s own capability. The Page loads its other data through its own app-only reads. A storefront whose every active source is your own external sales agent has no Interchange analytics to show, so the Page shows the connected agent instead, with the plan it is on (Listing, or Listing and Distribution) and the next steps that apply: Upgrade to Public Distribution or View plan & billing, Connect an ad server when merchandising is included but no source can supply products yet, and Review source readiness when such a source exists but is not ready. The server decides which view and which steps; the Page only shows them. Two app-only tools serve this view: seller_dashboard_get_mode reads the view and its steps, and seller_dashboard_open_page opens only Plan & Billing, ad-server connection, or one source’s diagnostics. Like the Page’s other reads, they are not model-visible and refuse any call that does not carry the Page’s own capability. 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. To count a kind’s objects by one field instead of listing them, add groupBy; see Count objects with search.

Developer accounts

A Developer account is the MCP home for building agents. get_status returns the account’s bounded agent list plus its next actions: connect_agent and connect_buyer_agent to register an agent you already run, set_up_hosted_agent (listed as unavailable, with the reason, until hosted agents are offered to Developer accounts), the call that unblocks the agent furthest from done, and open_agents_page, which opens the same view as a page. See build an agent or call search({ kind: "agent" }). In this account, search and get can read the account’s agents and the organization’s buyer_agent principals. save_buyer_agent manages those buyer agents: send no id to create one, or an id with exactly one change: rename, lifecycle state, credential handoff, or exact advertiser grants. Grants still name advertisers inside the organization boundary; a credential is never a grant. Other buyer and seller operations remain absent. Use save_agent with no agentId to register an agent from a Developer Account. Registration requires an account administrator, displayName, kind and an idempotencyKey. kind is sales or creative; any other value is refused. You can select an existing claimed operator or omit operator to create one with the Agent’s name. Create a buyer agent with save_buyer_agent instead: it takes no operator and no idempotencyKey, and the result is a buyer_agent carrying its principalId. A repeated buyer-agent request creates another buyer agent, so search before you retry. Send save_agent with an owned agentId and a new displayName to rename it, certificationRequested to allow certification buys for a Sales Agent (true) or to stop sending new ones (false), or both. search and get return certificationRequested for each agent. See Allow certification buys. Endpoint declarations are immutable revisions, so register them from the Agent Page rather than through save_agent.

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. A Buyer can browse the selected advertiser’s visible products across eligible Sellers before creating a campaign:
Each result includes the exact opaque productId, sellerAccountId, 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. When a Seller answers that your account with it needs setup, its row in sellerOutcomes is finished, not partial: outcome is skipped with reason_code account_needs_attention and a suggestion that points to Media Partners, and the text says the same. A page can hold fewer rows than limit even when more products are waiting. Each response keeps its product rows to a fixed amount of text, so every row an MCP host shows arrives whole, with its productId and pricing option ids intact. The rows that don’t fit move to the next page: the text says how many were held back, and nextCursor resumes at the first of them. Keep following nextCursor until a page returns none before treating the list as complete, even when a seller’s product count is higher than the rows you have read. Once resultsComplete is true, the response also carries productQueryId: the same id on every complete page of one search. To book products from the search, pass it as idempotencyKey to save_media_buy together with campaignId, the product’s sellerAccountId, each productId unchanged, and bidPrice for auction-priced products (see Stage returned products). One search books into one campaign; search again to book into another. Each row also carries pricingOptions: the product’s AdCP pricing_options as the seller sent them (snake_case, for example pricing_option_id, pricing_model, currency, fixed_price or floor_price), at most ten per product. An option whose pricing_option_id is longer than 180 characters, or whose JSON is larger than 2 KB, is left out, so a row can list fewer options than the seller offers. Pass the chosen pricing_option_id as pricingOptionId when booking. A product with exactly one option books at that option without pricingOptionId; a product with several catalogued options is refused until you choose one, and the error lists their ids. To see whether each product can be bought on the advertiser’s connected ad account, add include: ["executionReadiness"]:
Each row then carries executionReadiness when its seller reports readiness (currently Meta, TikTok, Snap, LinkedIn and Google): a summary of whether the product can be bought on that account and what’s missing.
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. To list the options for one resource of one product, such as the Facebook Pages a product can run as, use include: ["executionResources"] with the product’s productId from the search and the slot’s kind as resourceKind:
Each result row is one option: providerId, and where the seller gives them name, eventType, customEventName, and relatedResource (the resource it belongs to, such as the Page behind a lead form). The response’s resource repeats the slot summary for that kind, or is null when the seller reports none for this product (some sellers don’t report readiness at all). The first call asks the seller, so it can come back with resultsComplete: false, a pollAfterMs hint, and a nextCursor: call again with that cursor until resultsComplete is true. Pass nextCursor back to get the next page of options; keep the same filter and limit. A cursor stops working after about an hour without use; start again without one. Some sellers cap a list themselves: LinkedIn returns at most 100 lead forms or conversions, and Meta at most 200 Messenger welcome flows, Facebook Events, or product sets. A result carries overlaySupport when the Seller declares, in AdCP’s own overlay_support shape, which targeting a buyer may set on a package for that product. When it is present, it is the Seller’s complete declaration of the targeting that product supports. A product with no declaration omits the field, and its targeting follows the Seller’s own declaration, read with get on the Seller (see below). Each declaring product also carries supportedTargeting, the names of the dimensions it declares. A declaration too large to return inline is withheld rather than cut short, and so is any declaration past a page’s total inline budget (8 KB across its products): supportedTargeting is then present without overlaySupport. The text of the result lists each declared dimension with its systems, such as Nielsen DMA, and its limits. To read a product’s complete declaration, call get on that product with the productId from the search and the same advertiserId you searched with:
The result is the same row search returns, with overlaySupport in full (a seller that targets by region can declare tens of kilobytes of region codes). A declaration over 256 KB, far larger than any real one, is left out as search leaves one out, with supportedTargeting kept. It reads through the same buyer discovery as search, for that advertiser, so get finds only a product the search would return: any other productId answers NOT_FOUND. The text of the result stays short. It summarizes each declared dimension, with long value lists cut to eight and a count, and says that the complete declaration is in structuredContent. If the seller has not answered yet, get returns a retryable SERVICE_UNAVAILABLE: call it again. If the product’s Seller answered that your account with it needs setup (account_needs_attention), or is still being set up (account_pending), get returns BUYER_SETUP_REQUIRED with that reason in details.reason, and a message that points to Media Partners, instead of NOT_FOUND. A Seller Account reads its own product with sourceId and id instead, and never passes advertiserId. When the advertiser has property lists, a wholesale_product search leaves out any product that nothing can hold to those lists, and withholds any product Apostra could not check. It says so rather than returning an empty page that looks like no inventory. The result carries unenforceableProductCount and unverifiedProductCount, and each sellerOutcomes entry that had removals carries unenforceableProducts (up to 25, each with its productId, name, and the lists and reason that apply) or unverifiedProducts (productId and name), with a count and a ...Truncated flag. A withheld product may come back on a retry. The fields are absent when nothing was removed. See Property lists. 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:
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:
total counts the products that match your query and filter.productStatus across the sources read, not every product the Seller holds. active counts the active products on those sources before any filter, so read it beside total only when you sent no filter.

Conversation history

Buyer and Seller members can explicitly search their prior Apostra rooms with kind: "conversation":
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.

Sessions

Retained Session reads are available to signed-in, non-impersonated platform operators in seller accounts. An ordinary seller can directly search or get an explicitly source-qualified storefront_rfp_v1 Session or, when the server-owned compose-publication rollout selects that seller, a storefront_compose_run_v1 Session. Buyer Session access is staged by source. No buyer source is active today, so buyer Sessions is absent from the rail and buyer Session reads are denied. When an approved source is active and the server enables buyer reads for that account, the buyer can open the Sessions Page and use its buyer-visible reads. Buyers never receive revoked relationships, other buyers’ exchanges, unrelated sellers’ exchanges, private events, seller private notes, or coaching. save_session can add an idempotent private note. An ordinary seller may use it only for an authorised compose-run Session; use save_rfp_turn feedback for RFP-turn feedback. See the 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.
The MCP structuredContent response is limited to the write result:
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. For a non-impersonated user who can reach an eligible Seller Account, this tool can appear before that seller is selected. The call still requires the current seller’s compose-Session rollout and native-room permission check. Platform operators keep their existing seller-account access. See the Retained Session timeline for the complete access and retry rules.

save_lesson: manage seller guidance

save_lesson is available in a Seller Account context. Name the Seller Account with accountId. It uses four actions: propose requires a typed lesson with kind, scope, payload, seller words, and one provenance mode. A Teach this proposal uses sessionId, turnOrEventId, and noteId; its seller words must be verbatim note text. A general proposal that is not taught by a note uses sourceRevision and omits the Session and note ids. approve and retire each require a lessonId. preview requires a proposed lessonId and a brief. Preview is ephemeral: it writes no lesson, composition, provenance, cache entry, or seller history. A seller operator can propose guidance, including with Teach this from a saved Session note. Only a directly authenticated seller account administrator can approve or retire a lesson. A retired lesson cannot be approved again, so propose a new lesson to use that guidance again. An administrator acting through impersonation cannot approve or retire. See Teach from a Session note for the lesson limits and workflow.

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, modular_source for one a modular source offers from its avails, and agent_source for one a third-party sales agent publishes, which you read by naming that source’s sourceId. Both catalogue origins are the products Apostra 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. A Seller Account’s product also carries what was saved: pricingOptions in AdCP’s pricing option form (fixed_price or floor_price), deliveryType, channels as stored with any guess from its formats in inferredChannels, formatOptions as stored, inventory.publisher_properties, and prebidIntegrationActive. See Prices, delivery type and formats. For Buyer seller reads, filter.sellerAccountIds accepts up to 50 exact seller Account ids in one call, preserves requested order, and reports unavailable ids as objects.missingIds. filter.ids is refused on this read with an error that names filter.sellerAccountIds; it is still the filter for connection and creative_engine reads. 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. If the advertiser has no active account with that Seller, it is not searched and its catalog.ext.interchange.storefront_results[] row is skipped with reason_code account_pending (setup is on its way, nothing to do) or account_needs_attention (the buyer, an operator or the Seller has to act), and a suggestion that points to Media Partners. The text says the same on a Not searched line. include: ["products"] is served only on this Buyer Seller read, on proposal, and on media_buy. A Seller Account lists its own products with search({"kind":"wholesale_product"}). The Buyer Seller read also takes include: ["commitmentRecord"], the seller’s record against its goal commitments, and include: ["scores"], your own scores with that seller in each campaign you ran with it. See Goal-seeking campaigns. scores is served only on get, never on a seller search. A Seller Account asks for include: ["scores"] on its own seller read to see its own scores for each buyer and campaign, at its own net price and without ranks. See A seller’s own scores. get on a Buyer Seller also returns targeting: the targeting the Seller declares in its AdCP capabilities (media_buy.execution.targeting), with its systems and limits listed in the text. It applies to that Seller’s products that carry no overlay_support of their own. Alongside it, supportedTargeting names the dimensions the Seller declares. As for a product, a declaration too large to return inline is withheld rather than cut short: supportedTargeting is then present without targeting. Both fields are absent when the Seller declares no targeting, or stores targeting that is not valid AdCP. If the capability read fails, the whole get returns a retryable SERVICE_UNAVAILABLE rather than a Seller that looks as if it declares none. Every Buyer Seller read and search result also carries supportsProposals, a boolean independent of transactable: a seller can be fully transactable and still not support proposals. A wholesale-only seller — one that sells only fixed products through search(kind: "wholesale_product") and save_media_buy, with no brief or RFP path — declares supportsProposals: false. Check it before calling request_proposals; see Seller cohort rules for what request_proposals does with such a seller. Buyer search(kind:"seller") also accepts these filter fields to narrow results: Send either name of a filter, not both with different values. A request whose filter.sellerType and filter.classification, or filter.transactable and filter.marketplaceReady, disagree is refused.

Partner status

Every seller a buyer reads with search or get carries partnerStatus, your relationship with that seller: partnerStatus is read for your account. When the read names an advertiser, by filter.advertiserId on search or advertiserId on get, it is read for that advertiser, and the three values change in these ways:
  • ACTIVE means the advertiser qualifies for the seller, or the seller needs your own login and you have connected one, whether or not the advertiser qualifies.
  • NEEDS_CONNECTION is unchanged: a seller that needs your login and has no connection is NEEDS_CONNECTION whichever advertiser qualifies.
  • An automatic group or market maker selection does not count on its own, because it applies to the account. An advertiser the seller has no record of does not qualify.
A seller whose advertiser activation is still provisioning, deferred, or needs attention stays ACTIVE. The Media Partners page shows the same three groups as its Active, Needs connection, and Available tabs. partnerStatus is separate from connectionState.status, which is the technical state of the connection: connected, needs_account_selection, error, or not_connected. A seller can be ACTIVE with a connection that needs attention. search({"kind":"seller","filter":{"partnerStatus":["AVAILABLE"]}}) searches every seller listed on Apostra, so it can return sellers the default list hides. The default list, with neither filter.partnerStatus nor filter.marketplace, is unchanged. filter.partnerStatus combines with filter.sellerType, filter.transactable, filter.channel, filter.region, and query. It does not combine with filter.sellerAccountIds, which looks sellers up from the default list. filter.marketplace: true, now deprecated, is the same as listing all three values, and the deprecated name will be removed with the other old seller names.

Renamed seller fields

Seller reads and save_seller use Apostra names. The earlier names still work for now and are deprecated: reads return both, and save_seller accepts either. The deprecated names, including filter.marketplace, will be removed no earlier than 9 December 2026. Read the new names now. 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 carry sellerAccountId. Enrolled buyers can also connect a Creative Engine using target: {kind: "creative_engine", engineAccountId} (the engine’s Account 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 sellerAccountId to the seller catalog when names or Apostra 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. A connection also reports accountDiscovery: whether Apostra could read the ad accounts its sign-in reaches and, when it could not, why and who can fix it. See When Apostra can’t read a connection’s ad accounts.

Account inventory

get(kind:"connection", id, include:["inventory"], connectionAccountId) lists the event sources and audiences already on one provider account of the connection: the pixels, datasets and lists you map your own event sources and audiences to. connectionAccountId is an accounts[].id from the same connection read. The account is read about once an hour, so a new object can take up to an hour to appear, and the list holds names, counts and health only, never members or events. The page is object.inventory, 50 rows at a time. When inventory.page.nextOffset is not null, read again with connectionInventoryOffset set to it. Mapping an event source to one of these objects is save_event_source sellerAccounts[], with this upstreamObjectId as the platformId; mapping an audience is save_audience sellerAccounts[] the same way. Each accounts[] row of a get(kind:"connection") read also carries inventorySummary, so you can see what each account holds before reading its inventory: 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.

Organization accounts

An Organization account has no operating nouns. Its administrators read child accounts with search({kind: "account"}) and get({kind: "account"}), and the Organization’s get_status reports its child account counts, the accounts it uses and its Developer account; an organization has no limit on how many accounts it holds, and creation is capped at 25 per organization in 24 hours. get with include: ["lifecycle", "capacity"] adds one child’s archive state and blockers. save_account creates a child (no id), renames one, or archives and restores it with isArchived. Each of those writes returns an approval_required link, and an administrator of that organization approves the exact request on the website. Repeat the call with the same idempotencyKey to see the outcome, or send dryRun: true to preview. Send organizationId to act in an organization you are not connected to. The checks need a direct administrator of that organization, or a Scope3 platform operator (who alone can create a Demo Storefront); see Manage accounts with a V3 agent. Domains and operator identity. The Organization’s get_status also returns organization.domains: each domain the organization has claimed, with:
  • state: claimed, verifying while a DNS check is open, or verified;
  • proofMethod: dns_txt, staff_review or aao_sign_in, null until verified;
  • provedAt: null until verified;
  • accountIds: the child accounts that use the domain.
An organization that claims nothing returns domains: [], and null means the domains could not be read (a gap says so). Apostra staff supporting an organization can read its get_status overview, domains included, but cannot take its write actions, so their overview carries no save_account next step. Each account from search and get carries operatorIdentity:
  • domain;
  • unit: the account’s AdCP operator_unit.id, null while it has none;
  • proofState: proved or unproved.
operatorIdentity is null until the account chooses a domain. These fields are read-only: claiming and proving a domain is done on the website.

Internal Apostra administration

Directly authenticated Apostra SuperAdmins may also receive two internal tools: 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

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 also carries the organization’s plan, the offer it can accept at its exact version, its renewal and its recent invoices. See Billing: plan and invoices for those fields. The response omits card details, billing contact details, processor identifiers and browser credentials. When an organization manages billing for the selected account and you are not one of its administrators, the read does not fail. It returns who manages billing instead, the same answer Plan & Billing shows: object.organization.name names the organization, object.managedBy.customerId identifies it, and object.changeableBy is organization_administrator, because only an administrator of that organization can view or change its billing. The response carries none of the organization’s billing details. 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:
save_billing takes terms, paymentTerms, or both, or one plan declaration (plan, offerResponse or renewal, below), or a payer change to the payer details on invoices (below); a call sends at least one. It has no operation field. Sending Terms and payment terms together is two independent writes, Terms first. If the payment terms are then refused, the error says the Terms were accepted. 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.

Accept a plan, answer its offer or change its renewal

Three save_billing declarations act on the organization’s IU plan. Each is sent alone (not beside terms or paymentTerms), is pinned to the version of the offer or renewal the person was shown (read plan.offer.offerVersion and plan.renewal.version from get with kind: "billing", name a plan by the planId that read lists under plan.offer.plans, which is the plan’s code, and send a renewal action only from plan.renewal.allowedActions), takes an idempotencyKey, and needs the same direct organization authority as accepting Terms: a direct administrator of the organization, or a Scope3 staff member holding an active staff elevation for it (terms.canAccept on the billing read says whether the caller has it). A plan is narrower: only a direct administrator of the billing organization may ask for it or approve it, so a staff elevation does not apply. The governing Terms must already be accepted.
Accepting a plan commits the organization to its price, so a plan call never accepts on the call. It checks the plan against the current offer, records the request, and returns action: "approval_required" with a link to the Apostra site (Approving agent requests). Show the person the link. A direct administrator of the billing organization signs in, sees the exact plan, price, payment option and start, and approves or rejects it; the person who asked may approve it, and so may any other direct administrator. An administrator of a parent organization who is not a direct member, Apostra staff acting for the organization without a direct administrator membership, API keys and impersonated or support sessions cannot approve it. Nothing is accepted until someone approves, and the request expires after 24 hours. Repeat the same call with the same idempotencyKey to check the outcome. While the approval waits or runs it returns approval_required with its state. Once it has succeeded it returns action: "plan_accepted" with the plan’s bindingId, planId (the same plan code the billing read lists), planName, paymentOptionKey, termStart and termEnd. Its takesEffect is now when the plan is in force and at_term_end when it starts as the current term ends. The accepted plan is recorded as accepted by the administrator who approved it. A rejected, expired or failed approval accepts nothing; start again with a new idempotencyKey. offerResponse returns action: "offer_response_recorded" and renewal returns action: "renewal_changed", each on the call. Repeating a call with the same idempotencyKey and the same input returns the original result; reusing the key for a different choice is a CONFLICT. A version that is no longer current is refused and nothing changes. The error has code: "CONFLICT", field naming offerVersion or renewalVersion, and details with reason: "STALE_VERSION" and currentVersion, the version now in force. Show the person what changed, then send the new version only if they still agree. A caller without that authority, neither a direct organization administrator nor staff with an active elevation, receives ACCESS_DENIED, before any version is compared. Neither SHOWN nor a price is accepted, and a plan, offerResponse or renewal sent beside another field is refused by name. Apostra’s chat assistant follows the same path: it shows the approval link, and the plan is accepted only once an administrator approves it on the site. In the plan widget the person accepts in the widget. To ask Apostra for plan help or a private offer, file a save_ask of type commercial: the server attaches the organization’s plan context to it, so the caller does not send it.

Add billing details for consolidated billing

When get_status lists billing_details in blockers, the billing organization is missing a payer name, street address or country, and the buyer buys from a consolidated-billing seller. Add them with save_billing and payer, or in Plan and billing; the blocker clears on the next get_status. The billing read returns the details as payer, and payer.complete is false while any of the three is missing. payer is sent alone, takes only the fields to change (a field left out stays, null clears it) and an idempotencyKey, and returns action: "approval_required" with a link: a direct administrator of the billing organization approves the change on the Apostra site, and nothing is written until then. Repeat the same call with the same idempotencyKey to check the outcome; once approved it returns action: "payer_saved" with payer. A rejected, expired or failed approval changes nothing; start again with a new idempotencyKey. Billing: plan and invoices lists every field.

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:
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 and call save_billing. So can an organization administrator working inside a child account. In both, the write succeeds only for a direct administrator of the billing organization: for Terms, a plan or offer decision, a renewal change, payment terms and the card handoff, the service refuses anyone else. nextAction.canCompleteInChat is true when terms.canAccept is true and Terms are the next action. Payout setup stays on the Plan & Billing Page: its set_plan_billing_payout_payee action runs 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. For payment authority, call request_payment_authority, which takes no input beyond the account. It returns confirmation_required with a single-use confirmation token. Then call confirm_payment_authority with that confirmationToken. It returns human_action_required with the cardholder URL. Each is its own tool because each call is an event: a second request stages a new handoff, and a confirmation issues a link for a person to open. They need the same write permission and card-collection availability as the capture-link contract always did. That 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; this read is the status of the handoff, and there is no status tool. Its paymentAuthority fields distinguish current verification from setup history. status and expiresAt describe the latest capture link: save_billing no longer takes the earlier operation and paymentAuthority fields (request, confirm and status); a call that sends either is refused with a validation error. Send request_payment_authority, confirm_payment_authority and the billing read instead. The billing read’s paymentAuthority contains method: "capture_link", status and expiresAt for the latest capture link; no capture record returns status: null. It does not report the current payment method or submitted form state on its own: use paymentAuthority.verified and payment.paymentMethod 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

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 page for sequencing.

Update media-buy targeting

Use save_media_buy to update products[].targetingOverlay while the media buy is a draft, or on a live buy whose seller takes the change (liveChanges.changeTargeting is not not_accepted; send it alone). A buy that has left draft and whose seller does not take the change is refused before it is saved. See Change a live media buy. Country inputs must be ISO 3166-1 alpha-2 codes and region inputs must be ISO 3166-2 codes; names are display labels and are rejected as update input. The overlay can only narrow the campaign targeting. Each changed or cleared dimension is checked against the seller for that product before the update is saved. Omit a key to keep the buy-level value, or send null to clear it and inherit the campaign’s value again.

Inspect Seller operations

For a media buy, get includes a Seller-operation summary when work exists. Add include: ["operations"] for the bounded recent change list. A capped destination summary says truncated: true; a failed durable-store read says status: "unavailable", rather than implying no changes. See the media-buy read workflow for the states and follow-up actions.

Say whether a line item may start before every creative is approved

Pass startRequiresAllCreativesApproved: true on a save_media_buy product when you create the buy to keep that line item from going live before every creative on it is approved: the buy waits for the seller’s review instead of booking with some of the creatives. Omit it, or send false, to book with the approved creatives and attach the rest when they are approved. An update that names it returns NOT_IMPLEMENTED. See Creative sync outcomes.

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. AdCP’s standard names the vendor but not which of the vendor’s metrics holds the value, and a vendor can report several (for example more than one invalid traffic rate). To have Interchange judge an ivt, brand_safety or attention_score standard, name the metric in ext.apostra.metric_id, the identifier from the vendor’s own metric list (lowercase letters, digits and underscores). It needs vendor too, and is refused on viewability and completion_rate. Interchange uses it only to judge delivery: it never sends ext to the seller. ext accepts the apostra namespace only and refuses any other. Only V3 save_media_buy takes ext; the V2 media buy tools and reads do not carry it. This is an extension while the proposal to carry the metric on the AdCP standard itself is with AdCP; it is removed once AdCP ships the equivalent. Only send these terms for a product or seller that declared performance-standard support in discovery. At launch, Interchange forwards the standards, without ext.apostra, 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.

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 at least one identifier: a SHA-256 hashed_email (of the trimmed, lowercased email), a SHA-256 hashed_phone (of the E.164 number), or uids, AdCP’s platform user IDs such as { "type": "uid2", "value": "..." }. Hashes are lowercase hex. Hash emails and phone numbers before sending them: a member with a raw email, or with AdCP’s ext, is refused. Do not put raw PII in external_id.
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, sellerAccounts), with status, uploadedCount, matchedCount, lastOperationStatus, createdAt, and updatedAt beside them. status is AdCP’s AudienceStatus: processing, ready, or too_small. Target an audience once it is ready. An audience whose last sync failed has no status; it has an errors list with one AdCP error (code: "SERVICE_UNAVAILABLE", recovery: "transient"), and sending the same members again with save_audience retries it. lastOperationStatus is the latest sync’s state: processing, completed, or error. uploadedCount and matchedCount are Interchange’s own totals across the audience. 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. To say which audience already on a seller account this audience is, add sellerAccounts to its entry: up to 50 pairs of accountId, the seller account’s accounts[].id from get({ kind: "connection", id: "CONNECTION_ID" }), and platformId, the id of the audience on that account’s platform (for example a Meta Custom Audience ID). One save can name at most 200 seller accounts across all of its audiences. The audience must already exist: save its members first, and map it once get lists it. An entry that deletes an audience cannot also map it.
Saving records the mapping and does not itself contact the seller. It also asks Interchange to check that the audience is on that seller account, and the result appears on the audience’s sellerAccounts entry for that account. An entry that only maps seller accounts sends no members, and when every entry only maps, the response’s operationId is null. Saving again with a different platformId changes the audience for that account. Accounts you leave out of a later save keep their mapping. The save is refused with REFERENCE_NOT_FOUND, naming the field at fault, and nothing is saved or sent, when an accountId is not an active, connected account of yours, or when the audienceId does not exist for the advertiser yet. The mappings are saved before any members are sent, so if sending members then fails, the mappings stay saved and the whole save can be sent again. To unmap an account, send it with unmap: true in place of platformId: { "accountId": "SELLER_ACCOUNT_ID", "unmap": true }. Unmapping ends Interchange’s mapping on that account only; it never deletes or changes the audience on the seller’s account. The account no longer appears in sellerAccounts, and mapping it again later starts it at pending. Unmapping an account that is not mapped changes nothing, and an account can be unmapped after its connection has been disconnected. While a live media buy still uses the mapping, the save is refused with INVALID_STATE on that account’s unmap field, naming the media buys, and nothing is saved or sent; unmap it once they have ended or been canceled. On the get read, sellerAccounts lists the seller accounts the audience is mapped to. It is empty until you map one. Each entry has the account’s storefront (id and name), its accountId, platformId, status, lastSyncedAt, match, and, while the account cannot use the audience yet, a blocker with a code and message. platformId is the audience id you mapped on that account, from the moment you save it. status says whether Interchange has confirmed it: pending, with a blocker naming what it is waiting for or what to fix, until the seller account confirms the audience, then synced. lastSyncedAt and match describe the audience last confirmed on the account, so while a changed mapping is pending they still describe the earlier audience. sellerAccounts[].match is the seller’s own size and match rate for the audience on that account, in the AdCP sync_audiences result shape: matched_count, effective_match_rate, and match_breakdown (per identifier type: id_type, submitted, matched, match_rate). It is null until the seller reports them. Sellers differ: the same audience can match 60% of its members on one account and 20% on another, and some sellers report no match rate at all.

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.
To say which pixel a source uses on a seller account, add sellerAccounts to its entry: up to 50 pairs of accountId, the seller account’s accounts[].id from get({ kind: "connection", id: "CONNECTION_ID" }), and platformId, the id of the pixel on that account’s platform (for example a Meta dataset or a Snap Pixel). The pixels on an account are listed by get({ kind: "connection", id: "CONNECTION_ID", include: ["inventory"], connectionAccountId: "SELLER_ACCOUNT_ID" }) (see Account inventory). Name the pixel on every account the source should run on: Interchange never picks one for you, even when the account has only one. The one exception is a source whose own eventSourceId is exactly the id of a pixel on the account: with no mapping named, it uses that pixel. A pixel that lists the event types it accepts must accept every type in the source’s eventTypes; a source without eventTypes binds only to a pixel that lists none. One save can name at most 200 seller accounts across all of its event sources; a larger save is refused with VALIDATION_ERROR before anything is saved.
Saving records the request and does not contact the seller. When the account’s inventory already lists the pixel with the event types it accepts, and they do not cover the source’s eventTypes (a source without eventTypes is used for every type, so a pixel that lists any types does not cover it), the entry fails with VALIDATION_ERROR on that entry’s platformId field and the source is not saved; name a pixel that accepts those types or change the source’s eventTypes. A pixel the inventory does not list yet, for example because the account has not been read since you connected it, is saved as named. Interchange checks the pixel when the source syncs to the account. Saving a new or changed pixel starts that check straight away, so sellerAccounts shows the pixel synced or names what to fix without waiting for a media buy. Saving the same pixel again starts nothing new. The check sends none of your conversion data, so it does not depend on the Conversion events switch. See how a source reaches each seller account and Use your event sources and audiences on each seller account. Saving again with a different platformId 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. To unmap an account, send it with unmap: true in place of platformId: { "accountId": "SELLER_ACCOUNT_ID", "unmap": true }. Unmapping ends Interchange’s binding on that account only; it never deletes or changes the pixel on the seller’s account. The account no longer appears in sellerAccounts, a new media buy on it fails as it would for a source that was never bound there, and naming a pixel again later starts the account at pending until Interchange confirms it. Unmapping an account that is not bound changes nothing, and an account can be unmapped after its connection has been disconnected. While a live media buy uses the binding, the entry fails with INVALID_STATE on that account’s unmap field, naming the media buys, and the entry is not saved; unmap it once they have ended or been canceled. 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.
    • INVALID_STATE (correctable): a sellerAccounts entry asked to unmap an account that a live media buy still uses.
    • 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, platformId, status, lastSyncedAt, health, and, while the account cannot use the source yet, a blocker with a code and message. platformId is the pixel you named on that account, from the moment you save it; for a source that uses the pixel with its own id, it is that pixel once confirmed, and null before. status says whether Interchange has confirmed it: pending, with a blocker naming what it is waiting for or what to fix, until a sync confirms the pixel, then synced. When you change the pixel on an account that already has a confirmed one, platformId shows the new pixel and the account reads pending until Interchange confirms it, or names why the seller refused it. Meanwhile buys already on that account keep using the confirmed pixel, and lastSyncedAt and health still describe it. 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.

Log events

Use log_event to send conversion events to an event source the advertiser registered with save_event_source. One call covers one source and up to 10,000 events. Each event is an AdCP Event in AdCP’s snake_case fields: event_id, event_type, event_time, and optionally user_match, custom_data, action_source, surface, event_source_url, and custom_event_name. Some AdCP fields are accepted but not stored yet, among them surface, ext and custom_data.content_name. When an event carries one, the call succeeds and ingestion.warnings names each such field.
The whole call is accepted or refused. It is refused with VALIDATION_ERROR when an event is not a valid AdCP Event (for example a negative custom_data.value, a uids[].type outside AdCP’s list, or a custom_data.contents[] item without an id; over the HTTP API such a body is refused before the tool runs, with INVALID_REQUEST and HTTP 422), when an event breaks an AdCP rule the published schema does not carry (a user_match with no identifier, an empty user_match.uids list, or a watch_milestone event with neither custom_data.progress_percent nor custom_data.progress_seconds), when two events in the call share an event_type and event_id, when user_match.uids[].value, user_match.click_id, a surface string, or custom_data.content_name, content_category or search_string looks like a raw email address or phone number (send hashed_email or hashed_phone instead), when one of those three custom_data strings is longer than 256 characters, when the source does not accept an event’s event_type, or when the source declares valueCurrencies and an event with a custom_data.value has a missing or unlisted currency. It is refused with NOT_FOUND when the source is not registered on that advertiser. Resending an event in a later call is deduplicated by its event_type and event_id. Set testEventCode to mark the call as test traffic; the code is forwarded to the tracking service. The response’s ingestion object is the AdCP log_event result: events_received, events_processed, and, when present, partial_failures, warnings, and match_quality. Repeating a call with the same idempotencyKey and the same events returns the first result with replayed: true; the same key with different events is refused with CONFLICT. Confirm events are arriving with get({ kind: "event_source", id: "EVENT_SOURCE_ID", advertiserId: "ADVERTISER_ID" }). On every buyer read, sellerAccountId is the seller’s Account id; send it back to name that seller. The id of a seller’s own object on its platform, such as a Meta dataset, a Snap Pixel, or a Meta Custom Audience ID, is a platformId, as on sellerAccounts[].platformId on an event source or an audience. Neither is an internal customer ID. For one compatibility release, a seller’s deprecated connection.customerId remains available for existing integrations. Use sellerAccountId 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, including products a seller sells through its own sales agent: those are read from the agent’s published catalogue, and an account-specific catalogue is never read. Direct-adapter products keep their existing active-connection visibility rules:
productId is required to keep the catalogue read bounded. Add filter.sellerAccountId 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. Campaign collection membership uses addMembers and removeMembers. Each member names its memberType (creative or asset) and memberId; added members may also have a collection-specific label. Asset members are existing raw, source, brand, or generated assets, addressed by id only. The tool never accepts asset bytes, storage locations, provider identity, or signed URLs. addMemberIds and removeMemberIds remain deprecated aliases for saved creative members. Existing calls continue to work, with a limit of 500 IDs per field, but do not send either alias with addMembers or removeMembers in the same call.

Naming a seller by Account id

A seller is named by sellerAccountId, the seller’s Account id: the same kind of number as buyerAccountId and accountId. It is a different number from a Storefront id. The same digits can name two different sellers in the two spaces, so never copy a Storefront id into an Account-id input. Send sellerAccountId and buyerAccountId. Every V3 response that names a seller carries sellerAccountId, and every response that names the buyer’s Account carries buyerAccountId. That covers seller and connection reads, media buys, proposals, creative formats, wholesale products, request_proposals outcomes, get_status ready destinations and the seller’s own seller object and listing. A list carries sellerAccountIds (for example skippedSellerAccountIds on request_proposals, and appliedCohort.sellerAccountIds). Read the field there and send it back in the inputs below. An object’s own id is its Storefront id, and every field that refers to another seller, buyer or creative engine uses that party’s Account id. Responses no longer carry sellerId, storefrontId or buyerCustomerId, with two exceptions described below: a Seller’s own seller object and listing keep sellerId, holding the Account id, and a Demo Storefront keeps its own storefrontId. A call that still sends an old input name is refused with a VALIDATION_ERROR that names the replacement.
  • A Storefront id is a different number from the seller’s Account id, and the same digits can name two different sellers. Never copy one into the other.
  • The one seller id that is still a Storefront id is the id of a seller object a Buyer reads with search or get (kind: "seller"). It is what get({"kind":"seller","id"}) takes, and sellerAccountId sits beside it. A media buy, proposal or product row carries only sellerAccountId. To reach that seller’s object, call search({"kind":"seller","filter":{"sellerAccountIds":[...]}}) and pass the returned seller’s id to get. A Seller reading its own seller object or listing gets its Account id in id and sellerId, with sellerAccountId beside them.
  • A Demo Storefront (mode: "DEMO") keeps its storefrontId: it is the demo’s own id, and a reset gives the demo a new one under the same Account.
  • The sellerAccountId on a media buy’s operation destinations (operations.byDestination[]) is the seller’s Account id. The sellerAccountId of the Media Buys Page filter is different: it is the id of a seller-account mapping, not a seller’s Account id. Send that filter as sellerAccountMappingId, its accurate name; sellerAccountId there is refused with an error that names it. A campaign read carries sellerAccountIds, the Account ids of its sellers, and no longer the platformId slug list sellerIds; write a campaign’s sellers with sellerAccountIds.
  • An Account id with no live seller is refused with a VALIDATION_ERROR that names the field and the id, before anything is read or written.
  • A creative engine is named by engineAccountId, its Account id, in save_connection (target: {kind: "creative_engine", engineAccountId}), in open_creative_engines_page and in a save_creative_session draft’s engine. The old inputs, target.id for a creative engine and engineId, are refused with a VALIDATION_ERROR that names the replacement. engineAccountId is not the engine’s id or target.id: they are different numbers. Every response that names an engine carries engineAccountId: engines from search and get (kind: "creative_engine") keep their own id and carry engineAccountId beside it, while every reference to an engine, which is the target and engine of a creative engine connection, a Creative Session’s engine and the result of open_creative_engines_page, names it by engineAccountId alone. A connection’s target of kind seller likewise carries sellerAccountId and no id; a billing target keeps its id. An Account id that owns no live creative engine, or that owns a seller, is refused with a VALIDATION_ERROR that names the field and the id. Read the id from the engine; never copy an engine id into engineAccountId.
  • The other open_*_page tools are unchanged.

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. Add landing_page_url 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. A VAST asset, whether a tag URL or inline XML, also takes vastVersion (2.0, 3.0, 4.0, 4.1, 4.2, or 4.3). A declared version is kept even when the tag returns another, and the response’s warnings say so. Without one, Apostra uses the version it reads from the tag when it saves it; a managed ad server cannot take a tag with neither. The creative read returns the stored version as assets[].vastVersion. See Declare a VAST tag’s version. 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:
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:
To save an incomplete assembly, add "mode": "draft". A draft is owned by its advertiser, like every Creative, and can omit required media, copy, destination, or repeat-group slots. A draft that binds no files yet needs creativeFormatId, so you can save it before any slot is filled and bind files with sourceAssets on a later save. A draft selected with formatKind (and optional formatParams) binds its files in the save that creates it, because only a creativeFormatId draft can be revised. The response and every later Creative read return its state and whole-state stateRevision. The existing opaque revision remains the buyer-preview projection digest. Create a draft with advertiserId or with campaignId. A campaignId resolves the owning advertiser and records the campaign the Creative is meant for. If you send both, they must name the same advertiser. A draft never attaches, materializes, reviews, syncs, or delivers: it is not on the campaign, its media buys, or any seller until a "mode": "complete" save succeeds on the same creativeId. Only a new draft takes campaignId; revisions keep the campaign the draft was created with. 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. A draft that was not created with a campaignId stays in the advertiser Library without attaching to a campaign or contacting a provider. A draft that was created with a campaignId joins that campaign when completion succeeds, exactly as a Creative saved under campaignId does, and is sent to the media buys on it whose products accept its format. A completing save does not take a campaignId of its own. If the Creative is complete but cannot join the campaign, the response still returns the saved Creative with a warning naming the campaign and the error code. Attach it afterwards with creativeId, advertiserId, and campaignId, or campaignIds, as described below. 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. An attached Creative built from Library video or audio is added to every media buy on the campaign whose product accepts its format, like any other Creative; sellers receive a link to the media and fetch it once. 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. When a row is not_sent or failed, failureReason says why and, with it, who must act, and sellerErrors carries the structured part of what the seller named (never its free text); see Creative sync outcomes. 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 before new discovery or buying work; the account’s operating-unit code is generated from its name, or pass a short one as operator_unit. If the identity is locked, follow the support action from get_status instead, unless the account has no unit yet (operator_unit is null), which the same call can still assign. This confirms the commercial operator identity used with sellers; it does not add users, change account membership, or change the login organization.

See how a saved creative’s trackers expand

When the creative you save has trackers, the save_creative result for a create or an update carries macroPreview, and the result text summarizes it. It is a dry run of Apostra’s macro compiler on the saved creative’s impression and click trackers (a click tracker includes the creative’s own macroAdditions). It never fetches a URL and never uses a real identifier. A creative with no trackers has no macroPreview. To find which macros a seller resolves, read the creative format with get({ kind: "creative_format", id, advertiserId }). Its macro_resolution_capabilities lists, for each macro, who fills it and in which context; an absent entry means that format does not fill the macro. A creative tied to a format option (formatOptionRef) gets its fill set from those declarations. The full macro list is AdCP’s universal macros.

Launch a campaign with explicit confirmation

Launching with save_campaign is a two-call operation. First request a preview without changing the campaign:
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:
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. If an earlier launch booked media buys with their sellers and then stopped part-way, the campaign is still a draft but no buy is staged any more. The preview then lists the booked buys instead of reporting none, and each entry carries willStart: true for a paused buy that confirming resumes, false for a buy that stays as it is (still awaiting seller approval, already delivering, or paused on purpose by a buyer). Confirming resumes the paused buys that were not paused on purpose and moves the campaign out of draft. A buy a buyer paused on purpose stays paused; resume it with save_media_buy({ mediaBuyId, isPaused: false }). If a seller refuses a resume, for example while an ad set is still in review, the response lists that buy under execution.errors with the seller’s wording, the campaign stays a draft, and confirming again retries only the buys that are still paused. If the campaign still has staged buys as well, confirming sends them as usual and also resumes the booked buys that are waiting for launch; the preview lists both, with willStart on the booked ones. The staged buys decide whether the campaign goes live. A booked buy whose resume a seller refuses is listed under execution.errors; once the campaign is live, resume it with save_media_buy({ mediaBuyId, isPaused: false }). If a seller rejects a media buy during confirmation, the launch response keeps the top-level launch failure code and includes the seller’s bounded reason as fenced data in the affected buy’s execution detail. Read that media buy with get(kind: "media_buy") for the same bounded, neutralized reason in errorMessage; its text rendering is fenced as seller-provided data.

Cancel a campaign and its media buys

To cancel a campaign, send desiredPhase: "canceled" by itself with the campaign’s current expectedRevision:
This is irreversible: it cancels the campaign and every live media buy through its existing seller path. A guaranteed committed buy goes to operator approval and keeps delivering until approved. 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. To stop one buy without affecting its campaign or siblings, call save_media_buy with isCanceled: true; use isPaused: true to pause it instead. 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. Archiving a campaign changes only its visibility. It is refused while any of the campaign’s media buys is a draft, live, or otherwise not finished; the error lists each one. A restored campaign keeps the phase it had when it was archived, and its media buys are unchanged. A campaign deleted through the v2 REST DELETE cannot be restored; create a new one. save_media_buy archives a draft media buy only (isArchived: true); a dispatched buy cannot be archived. Request its cancellation with save_media_buy({ mediaBuyId, isCanceled: true }), then archive it once its cancellation is terminal. That cancellation may wait on seller or operator approval and must not be reported as done before it is. Read cancellationPending, not action, to tell whether it is still waiting. An archived media buy cannot be restored; stage a new one on the campaign. A completed buy stays as the delivery record, cannot be archived or canceled, and no longer delivers.

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:
Read it back with:
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.
For this example, the stages show {{USER_ID}} → {DEVICE_ID} → %%ADVERTISING_IDENTIFIER_PLAIN%%, while the final stage uses a visibly synthetic advertising identifier. sourceDialect uses AdCP’s macro dialect names: adcp, iab_vast or vendor. Leave it out and Apostra detects the dialect from the URL’s origin. AdCP’s iab_daast and unknown are refused, because Apostra holds no DAAST mapping and unknown describes a token rather than declaring a source. A vendor names its macro reference in sourceDialectNamespace, and may name an earlier or the current revision of that reference in sourceDialectRevision. Apostra applies its current mapping and the report returns the revision it applied; a later revision than Apostra has checked is refused. iab_vast needs no namespace. A vendor namespace Apostra has no mapping for needs a sourceDialectRevision; its tokens stay unresolved. Each entry of mappings describes one token with AdCP’s field names: token, dialect, dialect_namespace, dialect_revision, dialect_semantic, mapping_status and universal_semantic, plus Apostra’s own start and end offsets and registryVersion. mapping_status is verified_universal when the token maps to an AdCP universal macro, which universal_semantic then names, and unresolved otherwise. A token that is not a universal macro but is spelled in AdCP syntax, such as {NOPE}, has the dialect unknown. dialect_semantic is the dialect’s own name for the token and is left out when Apostra has none recorded. 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 macro_values, a map from an AdCP universal macro name without braces (such as DEVICE_ID) to its value, 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 Apostra 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:
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

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. get({"kind":"advertiser_access","advertiserId":"..."}) reads who has client access to one advertiser and the actions you can take now; see Read client access for an advertiser. Follow the Seller workflows page for sequencing. get({"kind":"seller","include":["readiness"]}) adds the setup checklist, the go-live state and progress, starter-brief coverage, the wholesale product count and source-health diagnoses. See Reading your seller’s setup checklist and go-live state. get({"kind":"seller","include":["operating"]}) adds whether buyers can use the seller now, whether it is paused or archived, and how it is routed. include:["entitlements"] adds what its plan includes. Both read the seller account’s own record; see Reading your seller’s operating state and plan. get({"kind":"seller","include":["teaching"]}) adds the Product marketing counts: claimCount, gapClaimCount and lastVerdictChangeAt. It is refused with ACCESS_DENIED for a seller outside the Product marketing cohort. See Reading your seller’s Product marketing counts. listing, playbook, business_rules, coverage, distribution, and notification_config are one per account, so get reads them without an id. If a call supplies an id for one of these kinds anyway, get ignores it, returns the account’s own object, and adds a warnings entry saying the id was ignored. billing still rejects an id, and in a Seller account get({"kind":"seller"}) must omit it.

Signals

save_signal writes a signal and get / search (kind: "signal") read it back, using AdCP’s own names. The draft takes one of two shapes, and its kind field chooses which: kind: "managed" for a signal on an ad-server source, or kind: "catalog" for a signal in the seller catalog.
  • Seller-catalog signal (kind: "catalog", no sourceId): a create needs signalId and keyType. countries lists the ISO 3166-1 alpha-2 countries where the signal may be used; leave it out for no restriction. You can change name, description, countries, isLive and access later; keyType, metadata and agentId are fixed at registration. Each access grant’s price is an AdCP VendorPricingOption (cpm, percent_of_media, flat_fee, per_unit or custom).
  • Ad-server signal (kind: "managed", with sourceId): a write replaces the whole signal, so send signalId, name, valueType and adapterConfig plus every optional field you keep. Prices are AdCP pricing_options, with pricing_option_id, max_cpm and unit_price as AdCP names them. Set targetingDimension to age for an age signal or property for a property signal; any other dimension makes a generic signal. save_signal refuses signalType and type.
  • Settings as entries. The ad-server mapping (adapterConfig) and a catalog signal’s metadata are lists of { key, value } entries, one per setting. key is the path to that setting: dotted names for nested objects (owner.team), a bracketed index for list items (groups[0].criteria[0].keyId), and a quoted name in brackets for a setting whose name holds a dot, a bracket or a space (["a.b"]). value is a string, a number, a boolean, a list of strings, null, or an empty object {}. For example, these entries:
    send { "kind": "audience_segment", "segment_id": "123" }. A key listed twice, a key that is both a value and the parent of another, or a gap in a list is refused. A read returns these settings exactly as stored, as a nested object. A managed write replaces the whole mapping, so to edit one setting, send every setting you keep as an entry, including any stored as null or {}.
  • Price options are AdCP’s. Each price is an AdCP vendor pricing option with AdCP’s field names and value rules: cpm, percent_of_media, flat_fee, per_unit or custom, each with a pricing_option_id. The published schema lists exactly those fields, so a model sees closed objects and no open map. It leaves out AdCP’s ext field, and the applies_to_output_format_ids and applies_to_output_capability_ids fields, which AdCP defines for creative transformers and says signals omit. A price that carries ext or those fields is still accepted and stored as sent.
  • The older open draft still works for now. A draft with no kind, an open object whose adapterConfig and metadata are nested objects, is still accepted for now. Send kind and entries instead: the open draft will be removed.

Agent event subscriptions

A buyer agent in the V3 beta can use save_notification_config with an eventSubscription change to create, update, or delete its own event subscription. A subscription references a verified webhook endpoint by ID; it never accepts a webhook URL or signing secret. Reporting subscriptions choose either interim-and-final updates or final-only updates. Both always receive the final update. Indicator subscriptions receive every update. get({"kind":"notification_config"}) returns the authenticated buyer agent’s event subscriptions alongside the account notification settings. It does not reveal another agent’s subscriptions or any endpoint credential. search({"kind":"proposal"}) lists one campaign’s proposals and requires filter.campaignId; advertiserId does not scope proposals. Read a proposal’s products with get({"kind":"proposal","id":"PROPOSAL_ID", "include":["products"]}). A composed product can carry wholesaleProductIds: the opaque sf2: routes for its active, buyer-visible wholesale catalogue products. Use one of those routes with search({"kind":"wholesale_product"}) to recognise the matching pricing and other catalogue details the buyer is allowed to read. The field is omitted when no active buyer-visible catalogue product matches; it never exposes seller-local identifiers or pricing by itself. The proposal read’s text also names each captured product and its pricing: a fixed product shows its fixed price and currency, while an auction product shows its floor, max-bid setting, and available p25, p50, and p75 guidance. The filter.status enum lists every kind’s statuses, some in two cases. When a listed value differs from the requested kind’s own status only in case, search reads it as that status: pending on an inventory-source search reads as PENDING, and OPEN on a library-request search reads as open. A value the kind does not have in any case is still refused, so a work-item search never treats a modular OPEN as an approval state.

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. Every inventory source also carries isAdapterSource. Add include:["poweredByAgent"] to read which Agent powers a source (null, {"state":"ACTIVE","agentId":"..."} or {"state":"REVOKED"}), and include:["testRuns"] for its latest five recorded source tests. See Reading an inventory source. 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, include:["webhookDeliveries"] for the agent’s recent inbound webhook outcomes with each refusal’s reason (details), 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.

Sales agents in a Seller account

In a Seller account, search({"kind":"agent"}) lists the sales agents the seller can add, and each result’s relationship says which kind it is:
  • own: a sales agent owned by any account in the seller’s organization (a parent and its child accounts count as one organization), for example one registered in the organization’s developer account.
  • partner: a certified partner’s live sales agent owned by another organization, with a current published connection setup that offers an authentication method, once the partner may operate agents for other organizations’ clients through the Partner Program.
Each result carries: The page also carries developerAccounts, the organization’s developer accounts as accountId and name (empty when it has none), and developerAccountsUrl, the account settings screen that creates one. See Connect to a partner agent. Add query to match an agent name, partner name or agent ID. A page holds at most 50 agents; pass the returned nextCursor as cursor for the next page. get({"kind":"agent","id":"<agentId>"}) reads any listed agent and returns the same fields. An agent the Seller account itself owns keeps its full read, where include and validationRunId apply. An ID that names no agent the Seller can add is not found. save_inventory_source with only an agent’s agentId connects it: an own agent opens the connection task bound to it, and a partner agent opens the partner connection form. A Seller account cannot create a sales agent from an endpointUrl alone; that create is refused with VALIDATION_ERROR. See Connect to a partner agent. The list is for the seller to choose from. It never tells a buyer which partner a seller uses, and Buyer accounts never receive it. A partner adds signupUrl to its published setup as described in Add a sign-up link to your connection setup. 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 guide defines the save_rfp and save_rfp_turn fields, polling states, complete response requirements, and clarification/failure behavior. The 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. Provenance on a structure node is a list of { key, value } entries when you send it. Completeness is reported per extraction facet (text, structure, tables, visuals, styles), and unsupported or degraded parsing is never presented as ready. See 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.

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: 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 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 for the full list. 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). 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:
Read one object with related detail:
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:
Read a selected documentation result by its returned document path:
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. 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, a failed seller, or local_refusal when the request was refused by Apostra’s own validation before it ever reached that seller (with a code, message, and an optional suggestion). 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. skipped with reasonCode account_pending or account_needs_attention means the campaign’s advertiser has no active account with that seller yet: setup is on its way, or someone has to act, and the seller’s error.message points to Media Partners. skipped with reasonCode round_predates_account_rule means the round was queued before requests recorded who sent them, so the seller’s company account could not be presented and the seller was not asked; the seller’s error.message says to run the request again, which asks it as the advertiser’s own account. A seller that is closed to buyers is never reported with an account reason: it is skipped with reasonCode eligibility, including a closed seller named by a buyer that seller sponsors. None of these sellers is contacted, because discovery asks a seller only as the advertiser’s own account with it. They appear only for a seller you named, through sellerAccountIds or the campaign seller list. When neither applies, request_proposals contacts only the eligible sellers that are on for the campaign’s advertiser and where the advertiser has an active account; the others are not contacted or listed. If none qualifies, the request fails with NO_ELIGIBLE_SELLERS and a message saying where to turn a media partner on, or how many sellers are still setting up the advertiser’s account and how many need attention. 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. 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:
request_proposals returns enough state to continue each seller branch:
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, Rate Limits, and Preview limitations.