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.Navigation and shared tools
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 useget_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 withsearch(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:
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"]:
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:
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:
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:
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 withkind: "conversation":
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-qualifiedstorefront_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.
structuredContent response is limited to the write result:
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
Forsearch({"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 withsearch 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:
ACTIVEmeans 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_CONNECTIONis unchanged: a seller that needs your login and has no connection isNEEDS_CONNECTIONwhichever 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.
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 andsave_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 withsearch({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,verifyingwhile a DNS check is open, orverified;proofMethod:dns_txt,staff_revieworaao_sign_in,nulluntil verified;provedAt:nulluntil verified;accountIds: the child accounts that use the domain.
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 AdCPoperator_unit.id,nullwhile it has none;proofState:provedorunproved.
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
Callget 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
Threesave_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.
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
Whenget_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
Whenget_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:
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 returnspaymentAuthority.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
Usesave_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
PassstartRequiresAllCreativesApproved: 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
PassperformanceStandards 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
Usesave_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.
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.
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
Usesave_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.
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.
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, orunchanged: the full event source, plussetup, which links the guide for sending events. Undoing an archive reportsupdated.archived: theeventSourceIdonly.failed: theeventSourceIdanderrorsin the AdCP error shape. Each error has acode, amessage, arecoveryhint, and, where it applies, thefieldat fault:VALIDATION_ERROR(correctable): the entry is invalid, for example a new source sent without aname.REFERENCE_NOT_FOUND(correctable): an entry that setsisArchivednamed aneventSourceIdthe advertiser does not have, or asellerAccountsentry named anaccountIdthat is not connected.INVALID_STATE(correctable): asellerAccountsentry 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 newidempotencyKey.
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
Uselog_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.
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 bysellerAccountId, 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
idthat is still a Storefront id is theidof a seller object a Buyer reads withsearchorget(kind: "seller"). It is whatget({"kind":"seller","id"})takes, andsellerAccountIdsits beside it. A media buy, proposal or product row carries onlysellerAccountId. To reach that seller’s object, callsearch({"kind":"seller","filter":{"sellerAccountIds":[...]}})and pass the returned seller’sidtoget. A Seller reading its ownsellerobject or listing gets its Account id inidandsellerId, withsellerAccountIdbeside them. - A Demo Storefront (
mode: "DEMO") keeps itsstorefrontId: it is the demo’s own id, and a reset gives the demo a new one under the same Account. - The
sellerAccountIdon a media buy’s operation destinations (operations.byDestination[]) is the seller’s Account id. ThesellerAccountIdof 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 assellerAccountMappingId, its accurate name;sellerAccountIdthere is refused with an error that names it. A campaign read carriessellerAccountIds, the Account ids of its sellers, and no longer theplatformIdslug listsellerIds; write a campaign’s sellers withsellerAccountIds. - An Account id with no live seller is refused with a
VALIDATION_ERRORthat names the field and the id, before anything is read or written. - A creative engine is named by
engineAccountId, its Account id, insave_connection(target: {kind: "creative_engine", engineAccountId}), inopen_creative_engines_pageand in asave_creative_sessiondraft’sengine. The old inputs,target.idfor a creative engine andengineId, are refused with aVALIDATION_ERRORthat names the replacement.engineAccountIdis not the engine’sidortarget.id: they are different numbers. Every response that names an engine carriesengineAccountId: engines fromsearchandget(kind: "creative_engine") keep their ownidand carryengineAccountIdbeside it, while every reference to an engine, which is thetargetandengineof a creative engine connection, a Creative Session’sengineand the result ofopen_creative_engines_page, names it byengineAccountIdalone. A connection’stargetof kindsellerlikewise carriessellerAccountIdand noid; a billing target keeps itsid. An Account id that owns no live creative engine, or that owns a seller, is refused with aVALIDATION_ERRORthat names the field and the id. Read the id from the engine; never copy an engineidintoengineAccountId. - The other
open_*_pagetools are unchanged.
Replay an uncertain advertiser create
When creating an advertiser, you may supply anidempotencyKey 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
Callsave_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:
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:
"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, thesave_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 withsave_campaign is a two-call operation. First request a
preview without changing the campaign:
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, senddesiredPhase: "canceled" by itself with the
campaign’s current expectedRevision:
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 existingadvertiser 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:
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.
{{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:
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", nosourceId): a create needssignalIdandkeyType.countrieslists the ISO 3166-1 alpha-2 countries where the signal may be used; leave it out for no restriction. You can changename,description,countries,isLiveand access later;keyType,metadataandagentIdare fixed at registration. Each access grant’spriceis an AdCPVendorPricingOption(cpm,percent_of_media,flat_fee,per_unitorcustom). -
Ad-server signal (
kind: "managed", withsourceId): a write replaces the whole signal, so sendsignalId,name,valueTypeandadapterConfigplus every optional field you keep. Prices are AdCPpricing_options, withpricing_option_id,max_cpmandunit_priceas AdCP names them. SettargetingDimensiontoagefor an age signal orpropertyfor a property signal; any other dimension makes a generic signal.save_signalrefusessignalTypeandtype. -
Settings as entries. The ad-server mapping (
adapterConfig) and a catalog signal’smetadataare lists of{ key, value }entries, one per setting.keyis 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"]).valueis 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 asnullor{}. -
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_unitorcustom, each with apricing_option_id. The published schema lists exactly those fields, so a model sees closed objects and no open map. It leaves out AdCP’sextfield, and theapplies_to_output_format_idsandapplies_to_output_capability_idsfields, which AdCP defines for creative transformers and says signals omit. A price that carriesextor those fields is still accepted and stored as sent. -
The older open
draftstill works for now. A draft with nokind, an open object whoseadapterConfigandmetadataare nested objects, is still accepted for now. Sendkindand entries instead: the open draft will be removed.
Agent event subscriptions
A buyer agent in the V3 beta can usesave_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 withget({"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.
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 fromopen_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: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:
document path:
Read structured results
Every tool returns MCPcontent 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:
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: trueas 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.